├── .coveragerc ├── .env-sample ├── .gitignore ├── .travis.yml ├── README.md ├── accounts ├── __init__.py ├── admin.py ├── apps.py ├── backends.py ├── cache.py ├── context_processors.py ├── factories.py ├── forms.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_userprofiles.py │ ├── 0003_auto_20160514_0528.py │ ├── 0004_auto_20160522_2139.py │ ├── 0005_auto_20160623_0657.py │ ├── 0006_auto_20161115_0635.py │ ├── 0007_auto_20170528_2250.py │ └── __init__.py ├── models.py ├── pipeline.py ├── signals.py ├── tests.py ├── urls.py └── views.py ├── api ├── __init__.py ├── apps.py ├── forms.py ├── migrations │ └── __init__.py ├── tests.py ├── urls.py └── views.py ├── artist ├── __init__.py ├── admin.py ├── apps.py ├── factories.py ├── forms.py ├── geolocator.py ├── managers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20160317_0521.py │ ├── 0003_auto_20160522_2139.py │ ├── 0004_auto_20160522_2141.py │ ├── 0005_auto_20160522_2328.py │ ├── 0006_updatetitles.py │ ├── 0007_artistadmin.py │ ├── 0008_auto_20160625_0134.py │ ├── 0009_auto_20170201_0753.py │ ├── 0010_auto_20170201_0754.py │ ├── 0011_auto_20170201_0754.py │ ├── 0012_auto_20170201_0820.py │ ├── 0013_auto_20170511_0508.py │ └── __init__.py ├── models.py ├── templatetags │ ├── __init__.py │ └── perdiem.py ├── tests.py └── views.py ├── campaign ├── __init__.py ├── admin.py ├── apps.py ├── factories.py ├── forms.py ├── managers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20160411_0246.py │ ├── 0003_auto_20160418_0057.py │ ├── 0004_artistpercentagebreakdown.py │ ├── 0005_auto_20160618_2310.py │ ├── 0006_auto_20160618_2351.py │ ├── 0007_auto_20160618_2352.py │ ├── 0008_auto_20160618_2352.py │ ├── 0009_auto_20160618_2353.py │ ├── 0010_auto_20160625_0134.py │ └── __init__.py ├── models.py ├── signals.py ├── tests.py └── views.py ├── emails ├── __init__.py ├── apps.py ├── exceptions.py ├── factories.py ├── mailchimp.py ├── managers.py ├── messages.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20160502_0538.py │ ├── 0003_auto_20160531_0446.py │ └── __init__.py ├── models.py ├── signals.py ├── tests.py ├── utils.py └── views.py ├── fabfile.py ├── manage.py ├── music ├── __init__.py ├── admin │ ├── __init__.py │ ├── forms.py │ ├── model_admins.py │ └── views.py ├── apps.py ├── factories.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20160725_0226.py │ ├── 0003_auto_20160730_1802.py │ ├── 0004_track.py │ ├── 0005_auto_20160911_0829.py │ ├── 0006_auto_20160920_0724.py │ ├── 0007_auto_20190908_2201.py │ └── __init__.py ├── models.py ├── templatetags │ ├── __init__.py │ └── music.py ├── tests.py └── views.py ├── perdiem ├── __init__.py ├── context_processors.py ├── gunicorn.py ├── nginx │ └── investperdiem.com ├── settings.py ├── tests.py ├── urls.py ├── views.py └── wsgi.py ├── poetry.lock ├── pyproject.toml ├── static ├── css │ ├── admin │ │ └── music │ │ │ └── daily-report.css │ ├── artist │ │ ├── artist_detail.css │ │ ├── artist_list.css │ │ └── artist_preview.css │ ├── extra │ │ ├── resources.css │ │ └── stats.css │ ├── home.css │ ├── music │ │ └── music.css │ ├── perdiem.css │ ├── profile │ │ └── profile.css │ └── vendor │ │ └── foundation.min.css ├── favicon.png ├── img │ ├── graph.svg │ ├── icons │ │ ├── perdiem_icon_114.png │ │ ├── perdiem_icon_144.png │ │ ├── perdiem_icon_57.png │ │ └── perdiem_icon_72.png │ ├── marketplace │ │ ├── amazon.svg │ │ ├── apple.svg │ │ ├── google.svg │ │ ├── itunes.svg │ │ ├── spotify.svg │ │ ├── tidal.svg │ │ └── youtube.svg │ ├── money.svg │ ├── monkeygif.gif │ ├── perdiem-all-devices.png │ ├── perdiem-anonymous-avatar.png │ ├── perdiem-background-profile.jpg │ ├── perdiem-background.jpg │ ├── perdiem-earnings.png │ ├── perdiem-home-stroke.svg │ ├── perdiem-home.svg │ ├── perdiem-invest.png │ ├── perdiem-logo.svg │ ├── perdiem-small.svg │ ├── pie.svg │ ├── resources │ │ ├── perdiem-anonymous.jpg │ │ ├── perdiem-avatar.jpg │ │ ├── perdiem-calculator.jpg │ │ ├── perdiem-cashout.jpg │ │ ├── perdiem-earnings.jpg │ │ ├── perdiem-emails.jpg │ │ ├── perdiem-media.jpg │ │ ├── perdiem-register.jpg │ │ └── perdiem-updates.jpg │ └── social-icon.jpg └── js │ ├── accounts │ └── login-errors.js │ ├── artist │ ├── artist-detail.js │ └── artist-list.js │ ├── vendor │ ├── Chart.min.js │ ├── foundation.min.js │ ├── jquery.cookie.min.js │ ├── jquery.min.js │ ├── modernizr.js │ └── smooth-scroll.js │ └── widgets │ └── coordinates.js └── templates ├── 404.html ├── 500.html ├── admin └── music │ └── activityestimate │ ├── change_list.html │ └── daily-report.html ├── artist ├── artist_application.html ├── artist_application_thanks.html ├── artist_detail.html ├── artist_list.html └── includes │ ├── artist_current_project.html │ ├── artist_detail_invest.html │ ├── artist_detail_overview.html │ ├── artist_detail_projects.html │ ├── artist_detail_updates.html │ ├── artist_past_campaigns.html │ ├── artist_update_media.html │ ├── artist_updates.html │ ├── auth_buttons.html │ ├── invest.html │ ├── investor_profile_artist_list.html │ └── share_buttons.html ├── base.html ├── email ├── artist_apply.email ├── artist_update.email ├── base.email ├── contact.email ├── email_verification.email ├── invest_success.email └── welcome.email ├── extra ├── artist-resources.html ├── faq.html ├── funding.html ├── investor-resources.html ├── privacy.html ├── terms.html └── trust.html ├── home.html ├── leaderboard └── leaderboard.html ├── music ├── album_detail.html └── music.html ├── object_preview.html ├── object_preview_music.html ├── registration ├── contact.html ├── contact_thanks.html ├── error │ ├── account-does-not-exist.html │ ├── account-exists.html │ └── email-required.html ├── includes │ ├── profile_earnings.html │ ├── profile_music.html │ ├── profile_portfolio.html │ ├── settings_avatar_form.html │ └── settings_form.html ├── password_reset_complete.html ├── password_reset_confirm.html ├── password_reset_done.html ├── password_reset_form.html ├── profile.html ├── public_profile.html ├── register.html ├── settings.html ├── unsubscribe.html └── verify_email.html └── widgets └── coordinates.html /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = ./ 3 | 4 | # Omit files from coverage that only run on prod 5 | omit = 6 | fabfile.py 7 | perdiem/gunicorn.py 8 | perdiem/settings/__init__.py 9 | perdiem/wsgi.py 10 | 11 | [report] 12 | exclude_lines = 13 | # Ignore pass (often used in abstract methods) 14 | pass 15 | -------------------------------------------------------------------------------- /.env-sample: -------------------------------------------------------------------------------- 1 | PERDIEM_SECRET_KEY='-3f5yh&(s5%9uigtx^yn=t_woj0@90__fr!t2b*96f5xoyzb%b' 2 | 3 | PERDIEM_DB_NAME=perdiem 4 | PERDIEM_DB_HOST=localhost 5 | PERDIEM_DB_USER=postgres 6 | 7 | # leave blank for no password 8 | PERDIEM_DB_PASSWORD= 9 | 10 | PERDIEM_GOOGLE_OAUTH2_KEY='1234-abc123.apps.googleusercontent.com' 11 | PERDIEM_GOOGLE_OAUTH2_SECRET='abc123' 12 | PERDIEM_FACEBOOK_KEY='1234' 13 | PERDIEM_FACEBOOK_SECRET='abc123' 14 | 15 | PERDIEM_STRIPE_PUBLIC_KEY='pk_test_abc123' 16 | PERDIEM_STRIPE_SECRET_KEY='sk_test_abc123' 17 | 18 | PERDIEM_MAILCHIMP_API_KEY='abc123-usX' 19 | PERDIEM_MAILCHIMP_LIST_ID='1234' 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - 3.8 5 | services: 6 | - memcached 7 | addons: 8 | postgresql: "9.6" 9 | 10 | before_install: 11 | - pip install poetry 12 | install: 13 | - poetry install 14 | 15 | before_script: 16 | - psql -c "CREATE DATABASE perdiem;" -U postgres 17 | - cp .env-sample .env 18 | 19 | script: 20 | - poetry run pyupgrade --py38-plus $(find . -name '*.py') 21 | - poetry run isort . --check 22 | - poetry run black . --check 23 | - poetry run python manage.py makemigrations --check # validate no outstanding model changes 24 | - poetry run python manage.py migrate 25 | - poetry run python manage.py collectstatic --no-input 26 | - poetry run coverage run ./manage.py test 27 | 28 | after_success: 29 | - bash <(curl -s https://codecov.io/bash) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PerDiem 2 | #### The world's first fan run record label 3 | 4 | [![Build Status](https://travis-ci.org/RevolutionTech/perdiem-django.svg?branch=master)](https://travis-ci.org/RevolutionTech/perdiem-django) 5 | [![codecov](https://codecov.io/gh/RevolutionTech/perdiem-django/branch/master/graph/badge.svg)](https://codecov.io/gh/RevolutionTech/perdiem-django) 6 | 7 | ## Setup 8 | 9 | ### Prerequisites 10 | 11 | PerDiem requires [PostgreSQL](https://www.postgresql.org/) and [memcached](http://memcached.org/) to be installed. 12 | 13 | ### Installation 14 | 15 | Use [poetry](https://github.com/sdispater/poetry) to install Python dependencies: 16 | 17 | poetry install 18 | 19 | ### Configuration 20 | 21 | PerDiem reads in environment variables from your local `.env` file. See `.env-sample` for configuration options. Be sure to [generate your own secret key](http://stackoverflow.com/a/16630719). 22 | 23 | With everything installed and all files in place, you may now create the database tables and collect static files. You can do this with: 24 | 25 | $ poetry run python manage.py migrate 26 | $ poetry run python manage.py collectstatic 27 | 28 | ### Deployment 29 | 30 | Before deploying, you will need to add some additional environment variables to your `.env` file. See `ProdConfig` for the environment variables used in production. 31 | 32 | ###### Note: The remainder of this section assumes that PerDiem is deployed in a Debian Linux environment. 33 | 34 | PerDiem uses Gunicorn with [runit](http://smarden.org/runit/) and [Nginx](http://nginx.org/). You can install them with the following: 35 | 36 | $ sudo apt-get install runit runit-systemd nginx 37 | 38 | The rest of the README assumes that the PerDiem repo was checked out in `/home/perdiem/`. Please replace this path as necessary. 39 | 40 | We need to copy the Nginx configuration: 41 | 42 | $ cd /etc/nginx/sites-enabled 43 | $ sudo ln -s /home/perdiem/perdiem-django/perdiem/nginx/investperdiem.com investperdiem.com 44 | 45 | Then we need to create a script to run PerDiem on boot with runit: 46 | 47 | $ sudo mkdir /etc/sv/perdiem 48 | $ cd /etc/sv/perdiem 49 | $ sudo nano run 50 | 51 | In this file, create a script similar to the following: 52 | 53 | #!/bin/sh 54 | 55 | GUNICORN=/home/perdiem/.cache/pypoetry/virtualenvs/perdiem-django-py3.8/bin/gunicorn 56 | ROOT=/home/perdiem/perdiem-django 57 | PID=/var/run/gunicorn.pid 58 | 59 | APP=perdiem.wsgi:application 60 | 61 | if [ -f $PID ]; then rm $PID; fi 62 | 63 | cd $ROOT 64 | exec $GUNICORN -c $ROOT/perdiem/gunicorn.py --pid=$PID $APP 65 | 66 | Then change the permissions on the file to be executable and symlink the project to /etc/service: 67 | 68 | $ sudo chmod u+x run 69 | $ sudo ln -s /etc/sv/perdiem /etc/service/perdiem 70 | 71 | PerDiem should now automatically be running on the local machine. 72 | 73 | To configure your local machine to enable easier deployments, simply add comma-separated SSH-like "host strings" for all of the production instances to an environment variable called `PERDIEM_REMOTE_HOSTS`. You may want to add this in your `.bashrc` or similar. Here is an example of a line in `.bashrc` that defines two PerDiem production instances: 74 | 75 | PERDIEM_REMOTE_HOSTS=user@host1.example.com,user@host2.example.com 76 | 77 | Then you will be able to deploy to all of your instances with Fabric, simply with: 78 | 79 | $ poetry run fab deploy 80 | 81 | If you'd like Fabric to notify your `#general` Slack channel when deployments complete, you can also add an environment variable `PERDIEM_DEPLOYBOT_TOKEN` containing the token for a bot configured on Slack: 82 | 83 | PERDIEM_DEPLOYBOT_TOKEN=abc123 84 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/accounts/__init__.py -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 12 May 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.contrib import admin 8 | from django.contrib.sites.models import Site 9 | from social_django.models import Association, Nonce, UserSocialAuth 10 | 11 | # Unregister Site and Python Social Auth models from admin 12 | admin.site.unregister(Site) 13 | for python_social_auth_model in [Association, Nonce, UserSocialAuth]: 14 | admin.site.unregister(python_social_auth_model) 15 | -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | 6 | name = "accounts" 7 | 8 | def ready(self): 9 | import accounts.signals 10 | -------------------------------------------------------------------------------- /accounts/backends.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 28 May 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from social_core.backends.facebook import FacebookOAuth2 8 | from social_core.backends.google import GoogleOAuth2 9 | 10 | 11 | class GoogleOAuth2Login(GoogleOAuth2): 12 | 13 | name = "google-oauth2-login" 14 | auth_operation = "login" 15 | 16 | def setting(self, name, default=None): 17 | return self.strategy.setting(name, default=default, backend=super()) 18 | 19 | 20 | class GoogleOAuth2Register(GoogleOAuth2): 21 | 22 | name = "google-oauth2-register" 23 | auth_operation = "register" 24 | 25 | def setting(self, name, default=None): 26 | return self.strategy.setting(name, default=default, backend=super()) 27 | 28 | 29 | class FacebookOAuth2Login(FacebookOAuth2): 30 | 31 | name = "facebook-login" 32 | auth_operation = "login" 33 | 34 | def setting(self, name, default=None): 35 | return self.strategy.setting(name, default=default, backend=super()) 36 | 37 | 38 | class FacebookOAuth2Register(FacebookOAuth2): 39 | 40 | name = "facebook-register" 41 | auth_operation = "register" 42 | 43 | def setting(self, name, default=None): 44 | return self.strategy.setting(name, default=default, backend=super()) 45 | -------------------------------------------------------------------------------- /accounts/cache.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from django.core.cache import cache 4 | 5 | 6 | def cache_using_pk(func): 7 | """ 8 | Given a model instance, cache the value from an instance method using the primary key 9 | """ 10 | 11 | @functools.wraps(func) 12 | def wrapper(instance, *args, **kwargs): 13 | cache_key = f"{func.__name__}-{instance.pk}" 14 | return cache.get_or_set( 15 | cache_key, functools.partial(func, instance, *args, **kwargs) 16 | ) 17 | 18 | return wrapper 19 | -------------------------------------------------------------------------------- /accounts/context_processors.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 9 July 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.conf import settings 8 | 9 | 10 | def keys(request): 11 | return { 12 | "FB_APP_ID": settings.SOCIAL_AUTH_FACEBOOK_KEY, 13 | "GA_TRACKING_CODE": settings.GA_TRACKING_CODE, 14 | "JACO_API_KEY": settings.JACO_API_KEY, 15 | } 16 | 17 | 18 | def profile(request): 19 | user = request.user 20 | if user.is_authenticated: 21 | return user.userprofile.profile_context() 22 | return {} 23 | -------------------------------------------------------------------------------- /accounts/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.apps import apps as django_apps 3 | from django.conf import settings 4 | 5 | 6 | def userfactory_factory(apps, has_password=True): 7 | class UserFactory(factory.django.DjangoModelFactory): 8 | 9 | _PASSWORD = "abc123" 10 | 11 | class Meta: 12 | model = apps.get_model(settings.AUTH_USER_MODEL) 13 | 14 | username = factory.Faker("user_name") 15 | email = factory.LazyAttribute(lambda user: f"{user.username}@gmail.com") 16 | 17 | if has_password: 18 | password = factory.PostGenerationMethodCall("set_password", _PASSWORD) 19 | 20 | return UserFactory 21 | 22 | 23 | UserFactory = userfactory_factory(apps=django_apps) 24 | 25 | 26 | class UserAvatarFactory(factory.django.DjangoModelFactory): 27 | class Meta: 28 | model = django_apps.get_model("accounts", "UserAvatar") 29 | 30 | user = factory.SubFactory(UserFactory) 31 | -------------------------------------------------------------------------------- /accounts/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 8 April 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.contrib.auth import login 8 | from django.utils.deprecation import MiddlewareMixin 9 | 10 | from accounts.forms import LoginAccountForm 11 | 12 | 13 | class LoginFormMiddleware(MiddlewareMixin): 14 | def process_request(self, request): 15 | if request.method == "POST" and "login-username" in request.POST: 16 | # Process the request as a login request 17 | # if login-username is in the POST data 18 | form = LoginAccountForm(data=request.POST, prefix="login") 19 | if form.is_valid(): 20 | login(request, form.get_user()) 21 | 22 | # We have to change the request method here because 23 | # the page the user is currently on might not support POST 24 | request.method = "GET" 25 | else: 26 | form = LoginAccountForm(request, prefix="login") 27 | 28 | # Add the login form to the request (accessible in context) 29 | request.login_form = form 30 | -------------------------------------------------------------------------------- /accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.6 on 2016-05-06 06:55 2 | import django.db.models.deletion 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="UserProfile", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("invest_anonymously", models.BooleanField(default=False)), 27 | ( 28 | "user", 29 | models.OneToOneField( 30 | on_delete=django.db.models.deletion.CASCADE, 31 | to=settings.AUTH_USER_MODEL, 32 | ), 33 | ), 34 | ], 35 | ) 36 | ] 37 | -------------------------------------------------------------------------------- /accounts/migrations/0002_userprofiles.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.6 on 2016-05-06 06:55 2 | from django.conf import settings 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 10 | ("accounts", "0001_initial"), 11 | ] 12 | 13 | def create_initial_userprofiles(apps, schema_editor): 14 | User = apps.get_model(settings.AUTH_USER_MODEL) 15 | UserProfile = apps.get_model("accounts", "UserProfile") 16 | for user in User.objects.all(): 17 | UserProfile.objects.create(user=user) 18 | 19 | operations = [ 20 | migrations.RunPython(create_initial_userprofiles, migrations.RunPython.noop) 21 | ] 22 | -------------------------------------------------------------------------------- /accounts/migrations/0003_auto_20160514_0528.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.6 on 2016-05-14 05:28 2 | import django.db.models.deletion 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ("accounts", "0002_userprofiles"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="UserAvatar", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ( 28 | "provider", 29 | models.CharField( 30 | choices=[ 31 | ("perdiem", "PerDiem"), 32 | ("google-oauth2", "Google"), 33 | ("facebook", "Facebook"), 34 | ], 35 | max_length=15, 36 | ), 37 | ), 38 | ( 39 | "user", 40 | models.ForeignKey( 41 | on_delete=django.db.models.deletion.CASCADE, 42 | to=settings.AUTH_USER_MODEL, 43 | ), 44 | ), 45 | ], 46 | ), 47 | migrations.CreateModel( 48 | name="UserAvatarImage", 49 | fields=[ 50 | ( 51 | "id", 52 | models.AutoField( 53 | auto_created=True, 54 | primary_key=True, 55 | serialize=False, 56 | verbose_name="ID", 57 | ), 58 | ), 59 | ("img", models.ImageField(upload_to=b"")), 60 | ( 61 | "avatar", 62 | models.OneToOneField( 63 | on_delete=django.db.models.deletion.CASCADE, 64 | to="accounts.UserAvatar", 65 | ), 66 | ), 67 | ], 68 | ), 69 | migrations.CreateModel( 70 | name="UserAvatarURL", 71 | fields=[ 72 | ( 73 | "id", 74 | models.AutoField( 75 | auto_created=True, 76 | primary_key=True, 77 | serialize=False, 78 | verbose_name="ID", 79 | ), 80 | ), 81 | ("url", models.URLField()), 82 | ( 83 | "avatar", 84 | models.OneToOneField( 85 | on_delete=django.db.models.deletion.CASCADE, 86 | to="accounts.UserAvatar", 87 | ), 88 | ), 89 | ], 90 | ), 91 | migrations.AddField( 92 | model_name="userprofile", 93 | name="avatar", 94 | field=models.ForeignKey( 95 | blank=True, 96 | null=True, 97 | on_delete=django.db.models.deletion.CASCADE, 98 | to="accounts.UserAvatar", 99 | ), 100 | ), 101 | migrations.AlterUniqueTogether( 102 | name="useravatar", unique_together={("user", "provider")} 103 | ), 104 | ] 105 | -------------------------------------------------------------------------------- /accounts/migrations/0004_auto_20160522_2139.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.6 on 2016-05-22 21:39 2 | from django.db import migrations, models 3 | 4 | import accounts.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("accounts", "0003_auto_20160514_0528")] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="useravatar", 14 | name="provider", 15 | field=models.CharField( 16 | choices=[ 17 | ("perdiem", "Custom"), 18 | ("google-oauth2", "Google"), 19 | ("facebook", "Facebook"), 20 | ], 21 | max_length=15, 22 | ), 23 | ), 24 | migrations.AlterField( 25 | model_name="useravatarimage", 26 | name="img", 27 | field=models.ImageField(upload_to=accounts.models.user_avatar_filename), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /accounts/migrations/0005_auto_20160623_0657.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.7 on 2016-06-23 06:57 2 | from django.conf import settings 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("accounts", "0004_auto_20160522_2139")] 9 | 10 | def usernames_to_lowercase(apps, schema_editor): 11 | User = apps.get_model(settings.AUTH_USER_MODEL) 12 | for user in User.objects.all(): 13 | user.username = user.username.lower() 14 | user.save() 15 | 16 | operations = [ 17 | migrations.RunPython(usernames_to_lowercase, migrations.RunPython.noop) 18 | ] 19 | -------------------------------------------------------------------------------- /accounts/migrations/0006_auto_20161115_0635.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.3 on 2016-11-15 06:35 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("accounts", "0005_auto_20160623_0657")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="useravatarurl", 12 | name="url", 13 | field=models.URLField(max_length=2000), 14 | ) 15 | ] 16 | -------------------------------------------------------------------------------- /accounts/migrations/0007_auto_20170528_2250.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.7 on 2017-05-28 22:50 2 | import django.db.models.deletion 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("accounts", "0006_auto_20161115_0635")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="userprofile", 13 | name="avatar", 14 | field=models.ForeignKey( 15 | blank=True, 16 | null=True, 17 | on_delete=django.db.models.deletion.SET_NULL, 18 | to="accounts.UserAvatar", 19 | ), 20 | ) 21 | ] 22 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /accounts/signals.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 5 May 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.contrib.auth.models import User 8 | from django.db import models 9 | from django.dispatch.dispatcher import receiver 10 | 11 | from accounts.models import UserProfile 12 | 13 | 14 | @receiver( 15 | models.signals.post_save, sender=User, dispatch_uid="post_user_create_handler" 16 | ) 17 | def post_user_create_handler(sender, **kwargs): 18 | user = kwargs["instance"] 19 | created = kwargs["created"] 20 | 21 | if created: 22 | UserProfile.objects.create(user=user) 23 | -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 26 July 2015 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.conf.urls import url 8 | from django.contrib.auth.views import ( 9 | LogoutView, 10 | PasswordResetCompleteView, 11 | PasswordResetConfirmView, 12 | PasswordResetDoneView, 13 | PasswordResetView, 14 | ) 15 | from django.views.generic import TemplateView 16 | 17 | from accounts.views import RegisterAccountView, SettingsView 18 | 19 | urlpatterns = [ 20 | url(r"^logout/?$", LogoutView.as_view(next_page="/"), name="logout"), 21 | url(r"^register/?$", RegisterAccountView.as_view(), name="register"), 22 | url(r"^settings/?$", SettingsView.as_view(), name="settings"), 23 | url( 24 | r"^password/reset/sent/?$", 25 | PasswordResetDoneView.as_view(), 26 | name="password_reset_done", 27 | ), 28 | url( 29 | r"^password/reset/complete/?$", 30 | PasswordResetCompleteView.as_view(), 31 | name="password_reset_complete", 32 | ), 33 | url( 34 | r"^password/reset/(?P[0-9A-Za-z_-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/?$", 35 | PasswordResetConfirmView.as_view(), 36 | name="password_reset_confirm", 37 | ), 38 | url(r"^password/reset/?$", PasswordResetView.as_view(), name="password_reset"), 39 | url( 40 | r"^error/email-required/?$", 41 | TemplateView.as_view(template_name="registration/error/email-required.html"), 42 | name="error_email_required", 43 | ), 44 | url( 45 | r"^error/account-exists/?$", 46 | TemplateView.as_view(template_name="registration/error/account-exists.html"), 47 | name="error_account_exists", 48 | ), 49 | url( 50 | r"^error/account-does-not-exist/?$", 51 | TemplateView.as_view( 52 | template_name="registration/error/account-does-not-exist.html" 53 | ), 54 | name="error_account_does_not_exist", 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/api/__init__.py -------------------------------------------------------------------------------- /api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = "api" 6 | -------------------------------------------------------------------------------- /api/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 29 May 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django import forms 8 | 9 | 10 | class CoordinatesFromAddressForm(forms.Form): 11 | 12 | address = forms.CharField() 13 | -------------------------------------------------------------------------------- /api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/api/migrations/__init__.py -------------------------------------------------------------------------------- /api/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 29 May 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.conf.urls import url 8 | 9 | from api.views import CoordinatesFromAddress, DeleteUpdate, PaymentCharge 10 | 11 | urlpatterns = [ 12 | url(r"^coordinates/?$", CoordinatesFromAddress.as_view()), 13 | url( 14 | r"^payments/charge/(?P\d+)/?$", 15 | PaymentCharge.as_view(), 16 | name="pinax_stripe_charge", 17 | ), 18 | url(r"^update/(?P\d+)/?$", DeleteUpdate.as_view()), 19 | ] 20 | -------------------------------------------------------------------------------- /artist/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/artist/__init__.py -------------------------------------------------------------------------------- /artist/admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 12 March 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django import forms 8 | from django.contrib import admin 9 | from django.contrib.admin.widgets import AdminTextInputWidget 10 | from django.template.loader import render_to_string 11 | from pagedown.widgets import AdminPagedownWidget 12 | 13 | from artist.models import Artist, ArtistAdmin, Bio, Genre, Photo, Playlist, Social 14 | 15 | 16 | class LocationWidget(AdminTextInputWidget): 17 | 18 | # TODO: Use template_name and refactor widget to use Django 1.11's new get_context() method 19 | # https://docs.djangoproject.com/en/1.11/ref/forms/widgets/#django.forms.Widget.get_context 20 | template_name_dj110_to_dj111_compat = "widgets/coordinates.html" 21 | 22 | def render(self, name, value, attrs=None, renderer=None): 23 | html = super().render(name, value, attrs=attrs, renderer=renderer) 24 | return html + render_to_string(self.template_name_dj110_to_dj111_compat) 25 | 26 | 27 | class ArtistAdminForm(forms.ModelForm): 28 | 29 | location = forms.CharField( 30 | help_text=Artist._meta.get_field("location").help_text, widget=LocationWidget 31 | ) 32 | 33 | class Meta: 34 | model = Artist 35 | fields = ("name", "genres", "slug", "location", "lat", "lon") 36 | 37 | 38 | class ArtistAdministratorInline(admin.StackedInline): 39 | 40 | model = ArtistAdmin 41 | raw_id_fields = ("user",) 42 | extra = 2 43 | 44 | 45 | class BioAdminForm(forms.ModelForm): 46 | 47 | bio = forms.CharField( 48 | help_text=Bio._meta.get_field("bio").help_text, widget=AdminPagedownWidget 49 | ) 50 | 51 | class Meta: 52 | model = Bio 53 | fields = ("bio",) 54 | 55 | 56 | class BioInline(admin.StackedInline): 57 | 58 | model = Bio 59 | form = BioAdminForm 60 | 61 | 62 | class PhotoInline(admin.TabularInline): 63 | 64 | model = Photo 65 | 66 | 67 | class PlaylistInline(admin.TabularInline): 68 | 69 | model = Playlist 70 | extra = 1 71 | 72 | 73 | class SocialInline(admin.TabularInline): 74 | 75 | model = Social 76 | 77 | 78 | class ArtistAdmin(admin.ModelAdmin): 79 | 80 | form = ArtistAdminForm 81 | prepopulated_fields = {"slug": ("name",)} 82 | inlines = ( 83 | ArtistAdministratorInline, 84 | BioInline, 85 | PhotoInline, 86 | PlaylistInline, 87 | SocialInline, 88 | ) 89 | 90 | 91 | admin.site.register(Genre) 92 | admin.site.register(Artist, ArtistAdmin) 93 | -------------------------------------------------------------------------------- /artist/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ArtistConfig(AppConfig): 5 | name = "artist" 6 | -------------------------------------------------------------------------------- /artist/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.apps import apps as django_apps 3 | from django.utils.text import slugify 4 | 5 | from accounts.factories import UserFactory 6 | 7 | 8 | def artistfactory_factory(apps): 9 | class ArtistFactory(factory.django.DjangoModelFactory): 10 | class Meta: 11 | model = apps.get_model("artist", "Artist") 12 | 13 | name = factory.Faker("user_name") 14 | slug = factory.LazyAttribute(lambda artist: slugify(artist.name)) 15 | 16 | # Willowdale, Toronto, Ontario, Canada 17 | lat = 43.7689 18 | lon = -79.4138 19 | 20 | return ArtistFactory 21 | 22 | 23 | def updatefactory_factory(apps): 24 | class UpdateFactory(factory.django.DjangoModelFactory): 25 | class Meta: 26 | model = apps.get_model("artist", "Update") 27 | 28 | artist = factory.SubFactory(artistfactory_factory(apps=apps)) 29 | 30 | return UpdateFactory 31 | 32 | 33 | ArtistFactory = artistfactory_factory(apps=django_apps) 34 | UpdateFactory = updatefactory_factory(apps=django_apps) 35 | 36 | 37 | class GenreFactory(factory.django.DjangoModelFactory): 38 | class Meta: 39 | model = django_apps.get_model("artist", "Genre") 40 | 41 | 42 | class ArtistAdminFactory(factory.django.DjangoModelFactory): 43 | class Meta: 44 | model = django_apps.get_model("artist", "ArtistAdmin") 45 | 46 | artist = factory.SubFactory(ArtistFactory) 47 | user = factory.SubFactory(UserFactory) 48 | -------------------------------------------------------------------------------- /artist/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 19 March 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django import forms 8 | from pagedown.widgets import PagedownWidget 9 | 10 | 11 | class ArtistApplyForm(forms.Form): 12 | 13 | artist_name = forms.CharField(label="Artist / Band Name") 14 | photo_link = forms.URLField( 15 | label="Artist Profile Photo (Download URL)", 16 | widget=forms.TextInput(attrs={"placeholder": "http://"}), 17 | ) 18 | genre = forms.CharField() 19 | location = forms.CharField() 20 | email = forms.EmailField() 21 | phone_number = forms.CharField() 22 | bio = forms.CharField( 23 | widget=forms.Textarea( 24 | attrs={"placeholder": "We started playing music because..."} 25 | ) 26 | ) 27 | project = forms.CharField( 28 | label="Project Name", 29 | widget=forms.TextInput(attrs={"placeholder": "Single/Album Name"}), 30 | ) 31 | campaign_reason = forms.CharField( 32 | label="What are you raising money for?", 33 | widget=forms.Textarea( 34 | attrs={"placeholder": "We are raising money to promote our album..."} 35 | ), 36 | ) 37 | amount_raising = forms.CharField( 38 | label="Amount Raising", widget=forms.TextInput(attrs={"placeholder": "$1,000"}) 39 | ) 40 | giving_back = forms.CharField( 41 | label="% Back To Investors", 42 | widget=forms.TextInput(attrs={"placeholder": "50%"}), 43 | ) 44 | campaign_start = forms.DateField( 45 | label="Campaign Start Date", 46 | widget=forms.TextInput(attrs={"placeholder": "MM/DD/YYYY"}), 47 | ) 48 | campaign_end = forms.DateField( 49 | label="Campaign End Date", 50 | widget=forms.TextInput(attrs={"placeholder": "MM/DD/YYYY"}), 51 | ) 52 | payback_period = forms.CharField( 53 | label="How long you want to pay back investors", 54 | widget=forms.TextInput(attrs={"placeholder": "5 years, 10 years, 20 years..."}), 55 | ) 56 | soundcloud = forms.URLField( 57 | label="SoundCloud", widget=forms.TextInput(attrs={"placeholder": "http://"}) 58 | ) 59 | spotify = forms.URLField( 60 | required=False, 61 | label="Spotify", 62 | widget=forms.TextInput(attrs={"placeholder": "http://"}), 63 | ) 64 | facebook = forms.URLField( 65 | required=False, widget=forms.TextInput(attrs={"placeholder": "http://"}) 66 | ) 67 | twitter = forms.CharField( 68 | required=False, widget=forms.TextInput(attrs={"placeholder": "@"}) 69 | ) 70 | instagram = forms.CharField( 71 | required=False, widget=forms.TextInput(attrs={"placeholder": "@"}) 72 | ) 73 | terms = forms.BooleanField( 74 | label="Terms & Conditions", 75 | help_text="I have read and agree to the Terms & Conditions", 76 | ) 77 | 78 | 79 | class ArtistUpdateForm(forms.Form): 80 | 81 | title = forms.CharField(max_length=75) 82 | text = forms.CharField(widget=PagedownWidget()) 83 | image = forms.ImageField(required=False) 84 | youtube_url = forms.URLField(required=False) 85 | 86 | def clean(self): 87 | cleaned_data = super().clean() 88 | image = cleaned_data["image"] 89 | youtube_url = cleaned_data["youtube_url"] 90 | provided = list(filter(lambda x: x, [image, youtube_url])) 91 | if len(provided) > 1: 92 | raise forms.ValidationError("Please only provide one image or video.") 93 | return cleaned_data 94 | -------------------------------------------------------------------------------- /artist/geolocator.py: -------------------------------------------------------------------------------- 1 | from geopy.geocoders import Nominatim 2 | 3 | geolocator = Nominatim(user_agent="PerDiem (+https://www.investperdiem.com/)") 4 | -------------------------------------------------------------------------------- /artist/migrations/0002_auto_20160317_0521.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.4 on 2016-03-17 05:21 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("artist", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="artist", 12 | name="slug", 13 | field=models.SlugField( 14 | help_text="A short label for an artist (used in URLs)", 15 | max_length=40, 16 | unique=True, 17 | ), 18 | ) 19 | ] 20 | -------------------------------------------------------------------------------- /artist/migrations/0003_auto_20160522_2139.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.6 on 2016-05-22 21:39 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("artist", "0002_auto_20160317_0521")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="bio", 12 | name="bio", 13 | field=models.TextField( 14 | help_text='Short biography of artist. Markdown syntax allowed, but no raw HTML. Examples: **bold**, *italic*, indent 4 spaces for a code block.' 15 | ), 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /artist/migrations/0004_auto_20160522_2141.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.6 on 2016-05-22 21:41 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("artist", "0003_auto_20160522_2139")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="update", 12 | name="text", 13 | field=models.TextField(help_text="The content of the update."), 14 | ) 15 | ] 16 | -------------------------------------------------------------------------------- /artist/migrations/0005_auto_20160522_2328.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.6 on 2016-05-22 23:28 2 | import django.db.models.deletion 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("artist", "0004_auto_20160522_2141")] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="UpdateImage", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | auto_created=True, 18 | primary_key=True, 19 | serialize=False, 20 | verbose_name="ID", 21 | ), 22 | ), 23 | ("img", models.ImageField(upload_to="artist/updates")), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name="UpdateMediaURL", 28 | fields=[ 29 | ( 30 | "id", 31 | models.AutoField( 32 | auto_created=True, 33 | primary_key=True, 34 | serialize=False, 35 | verbose_name="ID", 36 | ), 37 | ), 38 | ( 39 | "media_type", 40 | models.CharField( 41 | choices=[("image", "Image"), ("youtube", "YouTube")], 42 | max_length=8, 43 | ), 44 | ), 45 | ("url", models.URLField()), 46 | ], 47 | ), 48 | migrations.AddField( 49 | model_name="update", 50 | name="title", 51 | field=models.CharField(default="Update", max_length=75), 52 | preserve_default=False, 53 | ), 54 | migrations.AlterField( 55 | model_name="update", 56 | name="text", 57 | field=models.TextField(help_text="The content of the update"), 58 | ), 59 | migrations.AddField( 60 | model_name="updatemediaurl", 61 | name="update", 62 | field=models.ForeignKey( 63 | on_delete=django.db.models.deletion.CASCADE, to="artist.Update" 64 | ), 65 | ), 66 | migrations.AddField( 67 | model_name="updateimage", 68 | name="update", 69 | field=models.ForeignKey( 70 | on_delete=django.db.models.deletion.CASCADE, to="artist.Update" 71 | ), 72 | ), 73 | ] 74 | -------------------------------------------------------------------------------- /artist/migrations/0006_updatetitles.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.6 on 2016-05-22 23:28 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("artist", "0005_auto_20160522_2328")] 8 | 9 | def set_initial_update_titles(apps, schema_editor): 10 | Update = apps.get_model("artist", "Update") 11 | for update in Update.objects.all(): 12 | update.title = "{artist} Update: {date}".format( 13 | artist=update.artist.name, 14 | date=update.created_datetime.strftime("%m/%d/%Y"), 15 | ) 16 | update.save() 17 | 18 | operations = [ 19 | migrations.RunPython(set_initial_update_titles, migrations.RunPython.noop) 20 | ] 21 | -------------------------------------------------------------------------------- /artist/migrations/0007_artistadmin.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.6 on 2016-05-26 04:47 2 | import django.db.models.deletion 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ("artist", "0006_updatetitles"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="ArtistAdmin", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ( 28 | "role", 29 | models.CharField( 30 | choices=[ 31 | ("musician", "Musician"), 32 | ("manager", "Manager"), 33 | ("producer", "Producer"), 34 | ], 35 | help_text="The relationship of this user to the artist", 36 | max_length=12, 37 | ), 38 | ), 39 | ( 40 | "artist", 41 | models.ForeignKey( 42 | on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" 43 | ), 44 | ), 45 | ( 46 | "user", 47 | models.ForeignKey( 48 | on_delete=django.db.models.deletion.CASCADE, 49 | to=settings.AUTH_USER_MODEL, 50 | ), 51 | ), 52 | ], 53 | ) 54 | ] 55 | -------------------------------------------------------------------------------- /artist/migrations/0008_auto_20160625_0134.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.7 on 2016-06-25 01:34 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("artist", "0007_artistadmin")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="artistadmin", 12 | name="role", 13 | field=models.CharField( 14 | choices=[ 15 | ("musician", "Musician"), 16 | ("manager", "Manager"), 17 | ("producer", "Producer"), 18 | ("songwriter", "Songwriter"), 19 | ], 20 | help_text="The relationship of this user to the artist", 21 | max_length=12, 22 | ), 23 | ) 24 | ] 25 | -------------------------------------------------------------------------------- /artist/migrations/0009_auto_20170201_0753.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.5 on 2017-02-01 07:53 2 | import django.db.models.deletion 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("artist", "0008_auto_20160625_0134")] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Playlist", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | auto_created=True, 18 | primary_key=True, 19 | serialize=False, 20 | verbose_name="ID", 21 | ), 22 | ), 23 | ( 24 | "provider", 25 | models.CharField( 26 | choices=[("spotify", "Spotify"), ("soundcloud", "SoundCloud")], 27 | help_text="Provider of the playlist", 28 | max_length=10, 29 | ), 30 | ), 31 | ( 32 | "uri", 33 | models.TextField( 34 | help_text="URI that with the provider uniquely identifies a playlist" 35 | ), 36 | ), 37 | ( 38 | "artist", 39 | models.ForeignKey( 40 | on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" 41 | ), 42 | ), 43 | ], 44 | ), 45 | migrations.AlterUniqueTogether( 46 | name="playlist", unique_together={("provider", "uri")} 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /artist/migrations/0010_auto_20170201_0754.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.5 on 2017-02-01 07:54 2 | from django.db import migrations 3 | 4 | from artist.models import Playlist as PlaylistConst 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("artist", "0009_auto_20170201_0753")] 10 | 11 | def copy_soundcloud_playlists_to_playlist(apps, schema_editor): 12 | SoundCloudPlaylist = apps.get_model("artist", "SoundCloudPlaylist") 13 | Playlist = apps.get_model("artist", "Playlist") 14 | 15 | for scplaylist in SoundCloudPlaylist.objects.all(): 16 | Playlist.objects.get_or_create( 17 | artist=scplaylist.artist, 18 | provider=PlaylistConst.PLAYLIST_PROVIDER_SOUNDCLOUD, 19 | uri=scplaylist.playlist, 20 | ) 21 | 22 | # Note: Running the reverse migration on a database that contains playlists 23 | # from providers other than SoundCloud will result in data loss 24 | def copy_playlists_to_soundcloud_playlist(apps, schema_editor): 25 | SoundCloudPlaylist = apps.get_model("artist", "SoundCloudPlaylist") 26 | Playlist = apps.get_model("artist", "Playlist") 27 | 28 | for playlist in Playlist.objects.filter( 29 | provider=PlaylistConst.PLAYLIST_PROVIDER_SOUNDCLOUD 30 | ): 31 | SoundCloudPlaylist.objects.get_or_create( 32 | artist=playlist.artist, playlist=playlist.uri 33 | ) 34 | 35 | operations = [ 36 | migrations.RunPython( 37 | copy_soundcloud_playlists_to_playlist, copy_playlists_to_soundcloud_playlist 38 | ) 39 | ] 40 | -------------------------------------------------------------------------------- /artist/migrations/0011_auto_20170201_0754.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.5 on 2017-02-01 07:54 2 | from django.db import migrations 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("artist", "0010_auto_20170201_0754")] 8 | 9 | operations = [ 10 | migrations.RemoveField(model_name="soundcloudplaylist", name="artist"), 11 | migrations.DeleteModel(name="SoundCloudPlaylist"), 12 | ] 13 | -------------------------------------------------------------------------------- /artist/migrations/0012_auto_20170201_0820.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.5 on 2017-02-01 08:20 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("artist", "0011_auto_20170201_0754")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="updatemediaurl", 12 | name="media_type", 13 | field=models.CharField(choices=[("youtube", "YouTube")], max_length=8), 14 | ) 15 | ] 16 | -------------------------------------------------------------------------------- /artist/migrations/0013_auto_20170511_0508.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.7 on 2017-05-11 05:08 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("artist", "0012_auto_20170201_0820")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="update", 12 | name="text", 13 | field=models.TextField( 14 | help_text='The content of the update. Markdown syntax allowed, but no raw HTML. Examples: **bold**, *italic*, indent 4 spaces for a code block.' 15 | ), 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /artist/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/artist/migrations/__init__.py -------------------------------------------------------------------------------- /artist/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/artist/templatetags/__init__.py -------------------------------------------------------------------------------- /artist/templatetags/perdiem.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 25 May 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django import template 8 | 9 | register = template.Library() 10 | 11 | 12 | @register.filter 13 | def notrail_floatformat(num, digits): 14 | if int(num) == num: 15 | return int(num) 16 | return round(num, digits) 17 | -------------------------------------------------------------------------------- /campaign/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/campaign/__init__.py -------------------------------------------------------------------------------- /campaign/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CampaignConfig(AppConfig): 5 | 6 | name = "campaign" 7 | 8 | def ready(self): 9 | import campaign.signals 10 | -------------------------------------------------------------------------------- /campaign/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.apps import apps as django_apps 3 | 4 | from accounts.factories import UserFactory 5 | from artist.factories import artistfactory_factory 6 | 7 | 8 | def projectfactory_factory(apps): 9 | class ProjectFactory(factory.django.DjangoModelFactory): 10 | class Meta: 11 | model = apps.get_model("campaign", "Project") 12 | 13 | artist = factory.SubFactory(artistfactory_factory(apps=apps)) 14 | 15 | return ProjectFactory 16 | 17 | 18 | def campaignfactory_factory(apps, point_to_project=True): 19 | class CampaignFactory(factory.django.DjangoModelFactory): 20 | class Meta: 21 | model = apps.get_model("campaign", "Campaign") 22 | 23 | amount = 10000 24 | fans_percentage = 20 25 | 26 | # Allow the CampaignFactory to point to the artist 27 | # for migration test cases before the Project model was created 28 | if point_to_project: 29 | project = factory.SubFactory(projectfactory_factory(apps=apps)) 30 | else: 31 | artist = factory.SubFactory(artistfactory_factory(apps=apps)) 32 | 33 | return CampaignFactory 34 | 35 | 36 | def revenuereportfactory_factory(apps, point_to_project=True): 37 | class RevenueReportFactory(factory.django.DjangoModelFactory): 38 | class Meta: 39 | model = apps.get_model("campaign", "RevenueReport") 40 | 41 | # Allow the RevenueReportFactory to point to a campaign directly 42 | # for migration test cases before the Project model was created 43 | if point_to_project: 44 | project = factory.SubFactory(projectfactory_factory(apps=apps)) 45 | else: 46 | campaign = factory.SubFactory(campaignfactory_factory(apps=apps)) 47 | 48 | return RevenueReportFactory 49 | 50 | 51 | ProjectFactory = projectfactory_factory(apps=django_apps) 52 | CampaignFactory = campaignfactory_factory(apps=django_apps) 53 | RevenueReportFactory = revenuereportfactory_factory(apps=django_apps) 54 | 55 | 56 | class CustomerFactory(factory.django.DjangoModelFactory): 57 | class Meta: 58 | model = django_apps.get_model("pinax_stripe", "Customer") 59 | 60 | user = factory.SubFactory(UserFactory) 61 | 62 | 63 | class ChargeFactory(factory.django.DjangoModelFactory): 64 | class Meta: 65 | model = django_apps.get_model("pinax_stripe", "Charge") 66 | 67 | customer = factory.SubFactory(CustomerFactory) 68 | paid = True 69 | refunded = False 70 | 71 | 72 | class InvestmentFactory(factory.django.DjangoModelFactory): 73 | class Meta: 74 | model = django_apps.get_model("campaign", "Investment") 75 | 76 | charge = factory.SubFactory(ChargeFactory) 77 | campaign = factory.SubFactory(CampaignFactory) 78 | -------------------------------------------------------------------------------- /campaign/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 14 April 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django import forms 8 | 9 | 10 | class PaymentChargeForm(forms.Form): 11 | 12 | card = forms.CharField() 13 | num_shares = forms.IntegerField(min_value=1) 14 | 15 | def __init__(self, *args, **kwargs): 16 | self.campaign = kwargs.pop("campaign") 17 | super().__init__(*args, **kwargs) 18 | 19 | def clean_num_shares(self): 20 | num_shares = self.cleaned_data["num_shares"] 21 | if num_shares > self.campaign.num_shares_remaining(): 22 | raise forms.ValidationError( 23 | "The number of shares requested exceeds the number of shares available." 24 | ) 25 | return num_shares 26 | -------------------------------------------------------------------------------- /campaign/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class InvestmentManager(models.Manager): 5 | def filter_user_investments(self, user): 6 | return self.filter( 7 | charge__customer__user=user, charge__paid=True, charge__refunded=False 8 | ) 9 | -------------------------------------------------------------------------------- /campaign/migrations/0002_auto_20160411_0246.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.5 on 2016-04-11 02:46 2 | import django.db.models.deletion 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("pinax_stripe", "0003_make_cvc_check_blankable"), 10 | ("campaign", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField(model_name="investment", name="user"), 15 | migrations.AddField( 16 | model_name="investment", 17 | name="charge", 18 | field=models.OneToOneField( 19 | default=None, 20 | on_delete=django.db.models.deletion.CASCADE, 21 | to="pinax_stripe.Charge", 22 | ), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /campaign/migrations/0003_auto_20160418_0057.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.5 on 2016-04-18 00:57 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("campaign", "0002_auto_20160411_0246")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="investment", 12 | name="num_shares", 13 | field=models.PositiveSmallIntegerField( 14 | default=1, 15 | help_text="The number of shares an investor made in a transaction", 16 | ), 17 | ) 18 | ] 19 | -------------------------------------------------------------------------------- /campaign/migrations/0004_artistpercentagebreakdown.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.6 on 2016-05-30 00:10 2 | import django.db.models.deletion 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("artist", "0007_artistadmin"), 10 | ("campaign", "0003_auto_20160418_0057"), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="ArtistPercentageBreakdown", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ( 27 | "displays_publicly_as", 28 | models.CharField( 29 | help_text="The name shown on the artist's detail page", 30 | max_length=30, 31 | ), 32 | ), 33 | ( 34 | "percentage", 35 | models.FloatField( 36 | help_text="The percentage of revenue that goes back to this group/individual (a value from 0-100)" 37 | ), 38 | ), 39 | ( 40 | "artist_admin", 41 | models.ForeignKey( 42 | blank=True, 43 | null=True, 44 | on_delete=django.db.models.deletion.SET_NULL, 45 | to="artist.ArtistAdmin", 46 | ), 47 | ), 48 | ( 49 | "campaign", 50 | models.ForeignKey( 51 | on_delete=django.db.models.deletion.CASCADE, 52 | to="campaign.Campaign", 53 | ), 54 | ), 55 | ], 56 | ) 57 | ] 58 | -------------------------------------------------------------------------------- /campaign/migrations/0005_auto_20160618_2310.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.7 on 2016-06-18 23:10 2 | import django.db.models.deletion 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("artist", "0007_artistadmin"), 10 | ("campaign", "0004_artistpercentagebreakdown"), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="Project", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ( 27 | "reason", 28 | models.CharField( 29 | help_text="The reason why the artist is raising money, in a few words", 30 | max_length=40, 31 | ), 32 | ), 33 | ( 34 | "artist", 35 | models.ForeignKey( 36 | on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" 37 | ), 38 | ), 39 | ], 40 | ), 41 | migrations.AddField( 42 | model_name="campaign", 43 | name="project", 44 | field=models.ForeignKey( 45 | blank=True, 46 | null=True, 47 | on_delete=django.db.models.deletion.CASCADE, 48 | to="campaign.Project", 49 | ), 50 | preserve_default=False, 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /campaign/migrations/0006_auto_20160618_2351.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.7 on 2016-06-18 23:51 2 | from django.db import migrations 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("campaign", "0005_auto_20160618_2310")] 8 | 9 | def create_initial_projects(apps, schema_editor): 10 | Project = apps.get_model("campaign", "Project") 11 | Campaign = apps.get_model("campaign", "Campaign") 12 | for campaign in Campaign.objects.all(): 13 | project = Project.objects.create( 14 | artist=campaign.artist, reason=campaign.reason 15 | ) 16 | campaign.project = project 17 | campaign.save() 18 | 19 | operations = [ 20 | migrations.RunPython(create_initial_projects, migrations.RunPython.noop) 21 | ] 22 | -------------------------------------------------------------------------------- /campaign/migrations/0007_auto_20160618_2352.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.7 on 2016-06-18 23:52 2 | import django.db.models.deletion 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("campaign", "0006_auto_20160618_2351")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="campaign", 13 | name="project", 14 | field=models.ForeignKey( 15 | default=0, 16 | on_delete=django.db.models.deletion.CASCADE, 17 | to="campaign.Project", 18 | ), 19 | preserve_default=False, 20 | ), 21 | migrations.RemoveField(model_name="campaign", name="artist"), 22 | migrations.RemoveField(model_name="campaign", name="reason"), 23 | migrations.AddField( 24 | model_name="artistpercentagebreakdown", 25 | name="project", 26 | field=models.ForeignKey( 27 | blank=True, 28 | null=True, 29 | on_delete=django.db.models.deletion.CASCADE, 30 | to="campaign.Project", 31 | ), 32 | preserve_default=False, 33 | ), 34 | migrations.AddField( 35 | model_name="revenuereport", 36 | name="project", 37 | field=models.ForeignKey( 38 | blank=True, 39 | null=True, 40 | on_delete=django.db.models.deletion.CASCADE, 41 | to="campaign.Project", 42 | ), 43 | preserve_default=False, 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /campaign/migrations/0008_auto_20160618_2352.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.7 on 2016-06-18 23:52 2 | from django.db import migrations 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("campaign", "0007_auto_20160618_2352")] 8 | 9 | def point_artistpercentagebreakdown_to_project(apps, schema_editor): 10 | ArtistPercentageBreakdown = apps.get_model( 11 | "campaign", "ArtistPercentageBreakdown" 12 | ) 13 | for artistpercentagebreakdown in ArtistPercentageBreakdown.objects.all(): 14 | artistpercentagebreakdown.project = ( 15 | artistpercentagebreakdown.campaign.project 16 | ) 17 | artistpercentagebreakdown.save() 18 | 19 | def point_artistpercentagebreakdown_to_campaign(apps, schema_editor): 20 | ArtistPercentageBreakdown = apps.get_model( 21 | "campaign", "ArtistPercentageBreakdown" 22 | ) 23 | for artistpercentagebreakdown in ArtistPercentageBreakdown.objects.all(): 24 | artistpercentagebreakdown.campaign = ( 25 | artistpercentagebreakdown.project.campaign_set.first() 26 | ) 27 | artistpercentagebreakdown.save() 28 | 29 | def point_revenuereport_to_project(apps, schema_editor): 30 | RevenueReport = apps.get_model("campaign", "RevenueReport") 31 | for revenuereport in RevenueReport.objects.all(): 32 | revenuereport.project = revenuereport.campaign.project 33 | revenuereport.save() 34 | 35 | def point_revenuereport_to_campaign(apps, schema_editor): 36 | RevenueReport = apps.get_model("campaign", "RevenueReport") 37 | for revenuereport in RevenueReport.objects.all(): 38 | revenuereport.campaign = revenuereport.project.campaign_set.first() 39 | revenuereport.save() 40 | 41 | operations = [ 42 | migrations.RunPython( 43 | point_artistpercentagebreakdown_to_project, 44 | point_artistpercentagebreakdown_to_campaign, 45 | ), 46 | migrations.RunPython( 47 | point_revenuereport_to_project, point_revenuereport_to_campaign 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /campaign/migrations/0009_auto_20160618_2353.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.7 on 2016-06-18 23:53 2 | import django.db.models.deletion 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("campaign", "0008_auto_20160618_2352")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="artistpercentagebreakdown", 13 | name="project", 14 | field=models.ForeignKey( 15 | default=0, 16 | on_delete=django.db.models.deletion.CASCADE, 17 | to="campaign.Project", 18 | ), 19 | preserve_default=False, 20 | ), 21 | migrations.AlterField( 22 | model_name="revenuereport", 23 | name="project", 24 | field=models.ForeignKey( 25 | default=0, 26 | on_delete=django.db.models.deletion.CASCADE, 27 | to="campaign.Project", 28 | ), 29 | preserve_default=False, 30 | ), 31 | migrations.RemoveField(model_name="artistpercentagebreakdown", name="campaign"), 32 | migrations.RemoveField(model_name="revenuereport", name="campaign"), 33 | ] 34 | -------------------------------------------------------------------------------- /campaign/migrations/0010_auto_20160625_0134.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.7 on 2016-06-25 01:34 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("campaign", "0009_auto_20160618_2353")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="revenuereport", 12 | name="amount", 13 | field=models.DecimalField( 14 | decimal_places=2, 15 | help_text="The amount of revenue generated (in dollars) being reported (since last report)", 16 | max_digits=9, 17 | ), 18 | ) 19 | ] 20 | -------------------------------------------------------------------------------- /campaign/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/campaign/migrations/__init__.py -------------------------------------------------------------------------------- /campaign/signals.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 29 May 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.core.cache import cache 8 | from django.db import models 9 | from django.dispatch import receiver 10 | 11 | from accounts.models import UserProfile 12 | from campaign.models import Investment, RevenueReport 13 | 14 | 15 | @receiver( 16 | models.signals.post_save, 17 | sender=Investment, 18 | dispatch_uid="clear_leaderboard_from_investment_handler", 19 | ) 20 | @receiver( 21 | models.signals.post_save, 22 | sender=RevenueReport, 23 | dispatch_uid="clear_leaderboard_from_revenue_report_handler", 24 | ) 25 | def clear_leaderboard_cache_handler(sender, instance, **kwargs): 26 | cache.delete("leaderboard") 27 | 28 | 29 | @receiver( 30 | models.signals.post_save, 31 | sender=Investment, 32 | dispatch_uid="clear_profile_contexts_from_investment_handler", 33 | ) 34 | def clear_profile_context(sender, instance, **kwargs): 35 | pk = instance.investor().userprofile.pk 36 | cache.delete(f"profile_context-{pk}") 37 | 38 | 39 | @receiver( 40 | models.signals.post_save, 41 | sender=RevenueReport, 42 | dispatch_uid="clear_profile_contexts_from_revenue_report_handler", 43 | ) 44 | def clear_all_profile_contexts(sender, instance, **kwargs): 45 | # TODO(lucas): Review to improve performance 46 | # Instead of clearing out all of the profile contexts, we could just clear out 47 | # the profile contexts associated with the investors related to this revenue report 48 | cache_keys = [f"profile_context-{up.pk}" for up in UserProfile.objects.all()] 49 | cache.delete_many(cache_keys) 50 | -------------------------------------------------------------------------------- /campaign/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 29 May 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.core.cache import cache 8 | from django.db.models import Exists, OuterRef 9 | from django.views.generic import TemplateView 10 | 11 | from accounts.models import UserProfile 12 | from campaign.models import Investment 13 | 14 | 15 | class LeaderboardView(TemplateView): 16 | 17 | template_name = "leaderboard/leaderboard.html" 18 | 19 | @staticmethod 20 | def investor_context(investor): 21 | return { 22 | "name": investor.get_display_name(), 23 | "url": investor.public_profile_url(), 24 | "avatar_url": investor.avatar_url(), 25 | "amount": investor.get_total_earned(), 26 | } 27 | 28 | # TODO(lucas): Review to improve performance 29 | # Warning: top_earned_investors absolutely will not scale, the view is meant 30 | # to be run occasionally (once a day) and then have the whole page cached 31 | def calculate_leaderboard(self): 32 | user_profiles = UserProfile.objects.filter(invest_anonymously=False) 33 | investments = Investment.objects.filter_user_investments(user=OuterRef("pk")) 34 | investors = user_profiles.annotate(has_invested=Exists(investments)).filter( 35 | has_invested=True 36 | ) 37 | 38 | # Top earned investors 39 | top_earned_investors = [ 40 | self.investor_context(investor) for investor in investors 41 | ] 42 | top_earned_investors = list( 43 | filter(lambda context: context["amount"] > 0, top_earned_investors) 44 | ) 45 | top_earned_investors = sorted( 46 | top_earned_investors, key=lambda context: context["amount"], reverse=True 47 | )[:20] 48 | return top_earned_investors 49 | 50 | def get_context_data(self, **kwargs): 51 | context = super().get_context_data(**kwargs) 52 | leaderboard = cache.get_or_set("leaderboard", self.calculate_leaderboard) 53 | context["top_earned_investors"] = leaderboard 54 | return context 55 | -------------------------------------------------------------------------------- /emails/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/emails/__init__.py -------------------------------------------------------------------------------- /emails/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EmailsConfig(AppConfig): 5 | 6 | name = "emails" 7 | 8 | def ready(self): 9 | import emails.signals 10 | -------------------------------------------------------------------------------- /emails/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 17 April 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | 8 | class NoTemplateProvided(Exception): 9 | pass 10 | -------------------------------------------------------------------------------- /emails/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.apps import apps 3 | 4 | from accounts.factories import UserFactory 5 | 6 | 7 | class EmailSubscriptionFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = apps.get_model("emails", "EmailSubscription") 10 | 11 | user = factory.SubFactory(UserFactory) 12 | -------------------------------------------------------------------------------- /emails/mailchimp.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 14 May 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | import hashlib 8 | 9 | import requests 10 | from django.conf import settings 11 | 12 | 13 | class MailChimpException(Exception): 14 | def __init__(self, status_code, title, detail, type): 15 | message = "{status_code} {title}: {detail}\nMore information: {type}".format( 16 | status_code=status_code, title=title, detail=detail, type=type 17 | ) 18 | super().__init__(message) 19 | 20 | 21 | def update_user_subscription(email, subscribed): 22 | mailchimp_api_key = settings.MAILCHIMP_API_KEY 23 | mailchimp_data_center = mailchimp_api_key.split("-")[-1] 24 | url = "https://{dc}.api.mailchimp.com/3.0/lists/{list_id}/members/{subscriber_hash}".format( 25 | dc=mailchimp_data_center, 26 | list_id=settings.MAILCHIMP_LIST_ID, 27 | subscriber_hash=hashlib.md5(email.lower().encode("utf-8")).hexdigest(), 28 | ) 29 | status = "subscribed" if subscribed else "unsubscribed" 30 | data = {"email_address": email, "status": status} 31 | response = requests.put(url, json=data, auth=("", mailchimp_api_key)) 32 | if response.status_code >= 400: 33 | response_json = response.json() 34 | raise MailChimpException( 35 | status_code=response.status_code, 36 | title=response_json["title"], 37 | detail=response_json["detail"], 38 | type=response_json["type"], 39 | ) 40 | -------------------------------------------------------------------------------- /emails/managers.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 17 April 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.db import models 8 | 9 | 10 | class VerifiedEmailManager(models.Manager): 11 | def get_current_email(self, user): 12 | verified_email, _ = self.get_or_create(user=user, email=user.email) 13 | return verified_email 14 | 15 | def is_current_email_verified(self, user): 16 | verified_email = self.get_current_email(user) 17 | return verified_email.verified 18 | 19 | 20 | class EmailSubscriptionManager(models.Manager): 21 | def is_subscribed(self, user, subscription_type=None): 22 | if not subscription_type: 23 | subscription_type = self.model.SUBSCRIPTION_ALL 24 | 25 | try: 26 | subscription = self.get(user=user, subscription=subscription_type) 27 | except self.model.DoesNotExist: 28 | return True 29 | else: 30 | return subscription.subscribed 31 | 32 | def unsubscribe_user(self, user, subscription_type=None): 33 | if not subscription_type: 34 | subscription_type = self.model.SUBSCRIPTION_ALL 35 | self.update_or_create( 36 | user=user, subscription=subscription_type, defaults={"subscribed": False} 37 | ) 38 | -------------------------------------------------------------------------------- /emails/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.5 on 2016-04-18 00:57 2 | import django.db.models.deletion 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="EmailSubscription", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("subscribed", models.BooleanField(default=True)), 27 | ( 28 | "user", 29 | models.ForeignKey( 30 | on_delete=django.db.models.deletion.CASCADE, 31 | to=settings.AUTH_USER_MODEL, 32 | ), 33 | ), 34 | ], 35 | ) 36 | ] 37 | -------------------------------------------------------------------------------- /emails/migrations/0002_auto_20160502_0538.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.5 on 2016-05-02 05:38 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("emails", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.AddField( 11 | model_name="emailsubscription", 12 | name="subscription", 13 | field=models.CharField( 14 | choices=[("ALL", "General"), ("NEWS", "Newsletter")], 15 | default="ALL", 16 | max_length=6, 17 | ), 18 | ), 19 | migrations.AlterUniqueTogether( 20 | name="emailsubscription", unique_together={("user", "subscription")} 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /emails/migrations/0003_auto_20160531_0446.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.6 on 2016-05-31 04:46 2 | import uuid 3 | 4 | import django.db.models.deletion 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ("emails", "0002_auto_20160502_0538"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="VerifiedEmail", 19 | fields=[ 20 | ( 21 | "id", 22 | models.AutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("email", models.EmailField(max_length=254)), 30 | ("verified", models.BooleanField(default=False)), 31 | ( 32 | "code", 33 | models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), 34 | ), 35 | ( 36 | "user", 37 | models.ForeignKey( 38 | on_delete=django.db.models.deletion.CASCADE, 39 | to=settings.AUTH_USER_MODEL, 40 | ), 41 | ), 42 | ], 43 | ), 44 | migrations.AlterField( 45 | model_name="emailsubscription", 46 | name="subscription", 47 | field=models.CharField( 48 | choices=[ 49 | ("ALL", "General"), 50 | ("NEWS", "Newsletter"), 51 | ("ARTUP", "Artist Updates"), 52 | ], 53 | default="ALL", 54 | max_length=6, 55 | ), 56 | ), 57 | ] 58 | -------------------------------------------------------------------------------- /emails/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/emails/migrations/__init__.py -------------------------------------------------------------------------------- /emails/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 17 April 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | import uuid 8 | 9 | from django.contrib.auth.models import User 10 | from django.db import models 11 | from django.urls import reverse 12 | 13 | from emails.managers import EmailSubscriptionManager, VerifiedEmailManager 14 | 15 | 16 | class VerifiedEmail(models.Model): 17 | 18 | user = models.ForeignKey(User, on_delete=models.CASCADE) 19 | email = models.EmailField() 20 | verified = models.BooleanField(default=False) 21 | code = models.UUIDField(default=uuid.uuid4, unique=True, db_index=True) 22 | 23 | objects = VerifiedEmailManager() 24 | 25 | def __str__(self): 26 | return self.email 27 | 28 | def url(self): 29 | return reverse( 30 | "verify_email", kwargs={"user_id": self.user.id, "code": self.code} 31 | ) 32 | 33 | 34 | class EmailSubscription(models.Model): 35 | 36 | SUBSCRIPTION_ALL = "ALL" 37 | SUBSCRIPTION_NEWS = "NEWS" 38 | SUBSCRIPTION_ARTUP = "ARTUP" 39 | SUBSCRIPTION_CHOICES = ( 40 | (SUBSCRIPTION_ALL, "General"), 41 | (SUBSCRIPTION_NEWS, "Newsletter"), 42 | (SUBSCRIPTION_ARTUP, "Artist Updates"), 43 | ) 44 | 45 | user = models.ForeignKey(User, on_delete=models.CASCADE) 46 | subscription = models.CharField( 47 | choices=SUBSCRIPTION_CHOICES, max_length=6, default=SUBSCRIPTION_ALL 48 | ) 49 | subscribed = models.BooleanField(default=True) 50 | 51 | objects = EmailSubscriptionManager() 52 | 53 | class Meta: 54 | unique_together = (("user", "subscription"),) 55 | 56 | def __str__(self): 57 | return str(self.user) 58 | -------------------------------------------------------------------------------- /emails/signals.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 17 April 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.db import models 8 | from django.dispatch import receiver 9 | from pinax.stripe.models import Charge 10 | from pinax.stripe.webhooks import registry 11 | 12 | from emails.mailchimp import update_user_subscription 13 | from emails.messages import InvestSuccessEmail 14 | from emails.models import EmailSubscription 15 | 16 | 17 | @receiver( 18 | models.signals.pre_save, 19 | sender=EmailSubscription, 20 | dispatch_uid="unsubscribe_from_all_handler", 21 | ) 22 | def unsubscribe_from_all_handler(sender, instance, **kwargs): 23 | if ( 24 | instance.subscription == EmailSubscription.SUBSCRIPTION_ALL 25 | and not instance.subscribed 26 | ): 27 | for email_subscription in EmailSubscription.objects.filter( 28 | user=instance.user 29 | ).exclude(id=instance.id): 30 | email_subscription.subscribed = False 31 | email_subscription.save() 32 | 33 | 34 | @receiver( 35 | models.signals.pre_save, 36 | sender=EmailSubscription, 37 | dispatch_uid="sync_to_mailchimp_handler", 38 | ) 39 | def sync_to_mailchimp_handler(sender, instance, **kwargs): 40 | user_is_subscribed_news = EmailSubscription.objects.is_subscribed( 41 | user=instance.user, subscription_type=EmailSubscription.SUBSCRIPTION_NEWS 42 | ) 43 | if ( 44 | instance.subscription == EmailSubscription.SUBSCRIPTION_NEWS 45 | and instance.subscribed != user_is_subscribed_news 46 | ): 47 | update_user_subscription(instance.user.email, instance.subscribed) 48 | 49 | 50 | @receiver(registry.get_signal("charge.succeeded")) 51 | def charge_succeeded_handler(sender, **kwargs): 52 | # Get investment this successful charge is related to 53 | charge_id = kwargs["event"].message["data"]["object"]["id"] 54 | charge = Charge.objects.get(stripe_id=charge_id) 55 | investment = charge.investment 56 | 57 | # Send out email for investing 58 | InvestSuccessEmail().send(user=investment.investor(), investment=investment) 59 | -------------------------------------------------------------------------------- /emails/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 17 April 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from unittest import mock 8 | 9 | from django.test import TestCase, override_settings 10 | 11 | from emails.factories import EmailSubscriptionFactory 12 | from emails.models import EmailSubscription 13 | from emails.utils import create_unsubscribe_link 14 | from perdiem.tests import PerDiemTestCase 15 | 16 | 17 | class UnsubscribeTestCase(TestCase): 18 | def testUnsubscribeFromAllRemovesAllSubscriptions(self): 19 | # Create an artist update subscription 20 | email_subscription = EmailSubscriptionFactory( 21 | subscription=EmailSubscription.SUBSCRIPTION_ARTUP, subscribed=True 22 | ) 23 | 24 | # Create an explicit unsubscribe from all emails 25 | # We cannot use a factory to generate the EmailSubscription here 26 | # because we actually need the pre_save signal to be made 27 | EmailSubscription.objects.create( 28 | user=email_subscription.user, 29 | subscription=EmailSubscription.SUBSCRIPTION_ALL, 30 | subscribed=False, 31 | ) 32 | 33 | # Verify that when the user unsubscribes from everything, this artist update subscription is turned off 34 | email_subscription.refresh_from_db() 35 | self.assertFalse(email_subscription.subscribed) 36 | 37 | 38 | class SubscribeTestCase(PerDiemTestCase): 39 | def testSubscribeToNewsletterSuccess(self): 40 | self.assertResponseRenders( 41 | "/accounts/settings/", 42 | method="POST", 43 | data={ 44 | "action": "email_preferences", 45 | "email": self.user.email, 46 | "subscription_all": True, 47 | "subscription_news": True, 48 | "subscription_artist_update": False, 49 | }, 50 | ) 51 | 52 | 53 | class UnsubscribeWebTestCase(PerDiemTestCase): 54 | @classmethod 55 | def setUpTestData(cls): 56 | super().setUpTestData() 57 | EmailSubscriptionFactory(user=cls.user) 58 | 59 | def testUnsubscribe(self): 60 | unsubscribe_url = create_unsubscribe_link(self.user) 61 | self.assertResponseRenders(unsubscribe_url) 62 | 63 | def testUnsubscribeUnauthenticated(self): 64 | self.client.logout() 65 | unsubscribe_url = create_unsubscribe_link(self.user) 66 | self.assertResponseRenders(unsubscribe_url) 67 | 68 | def testUnsubscribeInvalidLink(self): 69 | self.client.logout() 70 | unsubscribe_url = "/unsubscribe/{user_id}/ALL/{invalid_token}/".format( 71 | user_id=self.user.id, invalid_token="abc123" 72 | ) 73 | response = self.assertResponseRenders(unsubscribe_url) 74 | self.assertIn(b"This link is invalid", response.content) 75 | 76 | @mock.patch("emails.mailchimp.requests.put") 77 | @override_settings( 78 | MAILCHIMP_API_KEY="FAKE_API_KEY", MAILCHIMP_LIST_ID="FAKE_LIST_ID" 79 | ) 80 | def testUnsubscribeFromMailChimp(self, mock_mailchimp_request): 81 | mock_mailchimp_request.return_value = mock.Mock(status_code=200) 82 | 83 | self.client.logout() 84 | 85 | # Simulate POST request received from MailChimp 86 | self.assertResponseRenders( 87 | "/unsubscribe/from-mailchimp/", 88 | method="POST", 89 | data={"data[list_id]": "FAKE_LIST_ID", "data[email]": self.user.email}, 90 | ) 91 | -------------------------------------------------------------------------------- /emails/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 17 April 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.core.signing import BadSignature, SignatureExpired, TimestampSigner 8 | from django.urls import reverse 9 | 10 | from emails.models import EmailSubscription 11 | 12 | 13 | def make_token(user): 14 | return TimestampSigner().sign(user.id) 15 | 16 | 17 | def create_unsubscribe_link(user, subscription_type=EmailSubscription.SUBSCRIPTION_ALL): 18 | user_id, token = make_token(user).split(":", 1) 19 | return reverse( 20 | "unsubscribe", 21 | kwargs={ 22 | "user_id": user_id, 23 | "subscription_type": subscription_type, 24 | "token": token, 25 | }, 26 | ) 27 | 28 | 29 | def check_token(user_id, token): 30 | try: 31 | key = f"{user_id}:{token}" 32 | TimestampSigner().unsign(key, max_age=60 * 60 * 48) # Valid for 2 days 33 | except (BadSignature, SignatureExpired): 34 | return False 35 | return True 36 | -------------------------------------------------------------------------------- /emails/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 17 April 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.conf import settings 8 | from django.contrib.auth.models import User 9 | from django.http import HttpResponse 10 | from django.shortcuts import get_object_or_404 11 | from django.views.decorators.csrf import csrf_exempt 12 | from django.views.generic import TemplateView 13 | 14 | from emails.models import EmailSubscription 15 | from emails.utils import check_token 16 | 17 | 18 | class UnsubscribeView(TemplateView): 19 | 20 | template_name = "registration/unsubscribe.html" 21 | 22 | def dispatch(self, request, *args, **kwargs): 23 | self.user = get_object_or_404(User, id=kwargs["user_id"]) 24 | return super().dispatch(request, *args, **kwargs) 25 | 26 | def get_context_data(self, **kwargs): 27 | context = super().get_context_data(**kwargs) 28 | 29 | user_is_logged_in = ( 30 | self.request.user.is_authenticated and self.request.user == self.user 31 | ) 32 | user_is_authenticated = user_is_logged_in or check_token( 33 | self.user, kwargs["token"] 34 | ) 35 | subscription_type = kwargs["subscription_type"] 36 | subscription_choices = dict(EmailSubscription.SUBSCRIPTION_CHOICES) 37 | 38 | if user_is_authenticated and subscription_type in subscription_choices: 39 | EmailSubscription.objects.unsubscribe_user( 40 | self.user, subscription_type=subscription_type 41 | ) 42 | context.update( 43 | { 44 | "success": True, 45 | "email": self.user.email, 46 | "subscription_type_display": subscription_choices[ 47 | subscription_type 48 | ], 49 | } 50 | ) 51 | else: 52 | context["success"] = False 53 | 54 | return context 55 | 56 | 57 | @csrf_exempt 58 | def unsubscribe_from_mailchimp(request): 59 | if ( 60 | request.method == "POST" 61 | and request.POST["data[list_id]"] == settings.MAILCHIMP_LIST_ID 62 | ): 63 | email = request.POST["data[email]"] 64 | try: 65 | user = User.objects.get(email=email) 66 | except User.DoesNotExist: 67 | pass 68 | else: 69 | EmailSubscription.objects.unsubscribe_user( 70 | user, subscription_type=EmailSubscription.SUBSCRIPTION_NEWS 71 | ) 72 | return HttpResponse("") 73 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 31 May 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | import os 8 | 9 | import requests 10 | from fabric import task 11 | 12 | PROJECT_DIR = "~/perdiem-django" 13 | REMOTE_HOSTS = os.environ.get("PERDIEM_REMOTE_HOSTS", "").split(",") 14 | 15 | remote_task = task(hosts=REMOTE_HOSTS) 16 | 17 | 18 | def _pull_latest_changes(cxn): 19 | """ 20 | Pull latest code from origin 21 | :return: Description of the changes since the last deploy 22 | """ 23 | with cxn.cd(PROJECT_DIR): 24 | previous_commit_hash = cxn.run( 25 | "git rev-parse HEAD", hide="stdout" 26 | ).stdout.strip() 27 | cxn.run("git pull", echo=True) 28 | cmd_changes_pulled = ( 29 | f"git log {previous_commit_hash}.. " 30 | f'--reverse --first-parent --format="%h : %an : %s" --no-color' 31 | ) 32 | changes_pulled = cxn.run(cmd_changes_pulled, hide="stdout").stdout.strip() 33 | return changes_pulled 34 | 35 | 36 | def _perform_update(cxn): 37 | """ 38 | Update dependencies, run migrations, etc. 39 | """ 40 | with cxn.prefix("export PATH=$HOME/.pyenv/bin:$HOME/.poetry/bin:$PATH"), cxn.cd( 41 | PROJECT_DIR 42 | ), cxn.prefix('eval "$(pyenv init -)"'): 43 | cxn.run("poetry install --no-dev", echo=True) 44 | cxn.run("poetry run python manage.py migrate", echo=True) 45 | cxn.run("poetry run python manage.py collectstatic --no-input", echo=True) 46 | 47 | 48 | def _send_notification(commits, deploy_successful): 49 | """ 50 | Post update to Slack 51 | """ 52 | bot_token = os.environ.get("PERDIEM_DEPLOYBOT_TOKEN") 53 | if not bot_token: 54 | return 55 | 56 | if commits: 57 | if deploy_successful: 58 | deploy_status = "completed successfully" 59 | else: 60 | deploy_status = "started, but was not completed successfully" 61 | text = f"Deploy {deploy_status}. Changelog:\n```\n{commits}\n```" 62 | elif deploy_successful: 63 | text = "Services were restarted successfully." 64 | else: 65 | text = "Attempted to restart services, but a failure occurred." 66 | 67 | data = {"token": bot_token, "channel": "#general", "text": text, "as_user": True} 68 | requests.post("https://slack.com/api/chat.postMessage", data=data) 69 | 70 | 71 | @remote_task 72 | def restart(cxn): 73 | """ 74 | Restart services 75 | """ 76 | cxn.sudo("sv restart perdiem", echo=True) 77 | cxn.sudo("service nginx restart", echo=True) 78 | 79 | 80 | @remote_task 81 | def deploy(cxn): 82 | """ 83 | Perform update, restart services, and send notification to Slack 84 | """ 85 | changes_pulled = _pull_latest_changes(cxn) 86 | 87 | deploy_successful = False 88 | try: 89 | _perform_update(cxn) 90 | restart(cxn) 91 | deploy_successful = True 92 | finally: 93 | _send_notification(changes_pulled, deploy_successful) 94 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | from dotenv import load_dotenv 6 | 7 | if __name__ == "__main__": 8 | load_dotenv() 9 | 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "perdiem.settings") 11 | os.environ.setdefault("DJANGO_CONFIGURATION", "BaseConfig") 12 | 13 | from configurations.management import execute_from_command_line 14 | 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /music/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/music/__init__.py -------------------------------------------------------------------------------- /music/admin/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 24 July 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.contrib import admin 8 | 9 | from music.admin.model_admins import ActivityEstimateAdmin, AlbumAdmin 10 | from music.models import ActivityEstimate, Album, Track 11 | 12 | admin.site.register(Album, AlbumAdmin) 13 | admin.site.register(Track) 14 | admin.site.register(ActivityEstimate, ActivityEstimateAdmin) 15 | -------------------------------------------------------------------------------- /music/admin/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 9 October 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django import forms 8 | from django.core.exceptions import ObjectDoesNotExist 9 | from pagedown.widgets import AdminPagedownWidget 10 | 11 | from music.models import ActivityEstimate, AlbumBio, Track 12 | 13 | 14 | class AlbumBioAdminForm(forms.ModelForm): 15 | 16 | bio = forms.CharField( 17 | help_text=AlbumBio._meta.get_field("bio").help_text, widget=AdminPagedownWidget 18 | ) 19 | 20 | class Meta: 21 | model = AlbumBio 22 | fields = ("bio",) 23 | 24 | 25 | class ActivityEstimateAdminForm(forms.ModelForm): 26 | class Meta: 27 | model = ActivityEstimate 28 | fields = ("date", "activity_type", "content_type", "object_id", "total") 29 | 30 | def clean(self): 31 | cleaned_data = super().clean() 32 | 33 | if not self.errors: 34 | # Get the object associated with this ActivityEstimate 35 | content_type = cleaned_data["content_type"] 36 | object_id = cleaned_data["object_id"] 37 | try: 38 | obj = content_type.get_object_for_this_type(id=object_id) 39 | except ObjectDoesNotExist: 40 | raise forms.ValidationError( 41 | "The {object_name} with ID {invalid_id} does not exist.".format( 42 | object_name=content_type.model, invalid_id=object_id 43 | ) 44 | ) 45 | 46 | # Get the album associated with this ActivityEstimate 47 | if hasattr(obj, "album"): 48 | album = obj.album 49 | else: 50 | album = obj 51 | 52 | # Verify that the associated album has a campaign defined 53 | if not album.project.campaign_set.all().exists(): 54 | raise forms.ValidationError( 55 | "You cannot create activity estimates without defining the revenue percentages " 56 | "issued to artists and fans. You must first create a campaign." 57 | ) 58 | 59 | return cleaned_data 60 | 61 | 62 | class DailyReportForm(forms.Form): 63 | 64 | track = forms.ModelChoiceField( 65 | queryset=Track.objects.all(), widget=forms.HiddenInput() 66 | ) 67 | streams = forms.IntegerField(min_value=0) 68 | downloads = forms.IntegerField(min_value=0) 69 | -------------------------------------------------------------------------------- /music/admin/model_admins.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 9 October 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.conf.urls import url 8 | from django.contrib import admin 9 | 10 | from music.admin.forms import ActivityEstimateAdminForm, AlbumBioAdminForm 11 | from music.admin.views import DailyReportAdminView 12 | from music.models import AlbumBio, Artwork, Audio, MarketplaceURL, Track 13 | 14 | 15 | class TrackInline(admin.StackedInline): 16 | 17 | model = Track 18 | extra = 1 19 | 20 | 21 | class ArtworkInline(admin.TabularInline): 22 | 23 | model = Artwork 24 | 25 | 26 | class AlbumBioInline(admin.StackedInline): 27 | 28 | model = AlbumBio 29 | form = AlbumBioAdminForm 30 | 31 | 32 | class MarketplaceURLInline(admin.TabularInline): 33 | 34 | model = MarketplaceURL 35 | 36 | 37 | class AudioInline(admin.TabularInline): 38 | 39 | model = Audio 40 | 41 | 42 | class AlbumAdmin(admin.ModelAdmin): 43 | 44 | raw_id_fields = ("project",) 45 | prepopulated_fields = {"slug": ("name",)} 46 | inlines = ( 47 | TrackInline, 48 | ArtworkInline, 49 | AlbumBioInline, 50 | MarketplaceURLInline, 51 | AudioInline, 52 | ) 53 | 54 | 55 | class ActivityEstimateAdmin(admin.ModelAdmin): 56 | 57 | list_display = ("content_object", "date", "activity_type") 58 | form = ActivityEstimateAdminForm 59 | 60 | def get_urls(self): 61 | urls = super().get_urls() 62 | custom_urls = [ 63 | url( 64 | r"^daily-report/?$", 65 | admin.site.admin_view(DailyReportAdminView.as_view()), 66 | name="daily_report", 67 | ) 68 | ] 69 | return custom_urls + urls 70 | -------------------------------------------------------------------------------- /music/admin/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 9 October 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.contrib import messages 8 | from django.urls import reverse 9 | 10 | from music.admin.forms import DailyReportForm 11 | from music.models import ActivityEstimate, Track 12 | from perdiem.views import FormsetView 13 | 14 | 15 | class DailyReportAdminView(FormsetView): 16 | 17 | template_name = "admin/music/activityestimate/daily-report.html" 18 | form_class = DailyReportForm 19 | 20 | def get_success_url(self): 21 | return reverse("admin:music_activityestimate_changelist") 22 | 23 | def get_context_data(self, **kwargs): 24 | context = super().get_context_data(**kwargs) 25 | context.update( 26 | { 27 | "title": "Enter Daily Report", 28 | "has_permission": self.request.user.is_superuser, 29 | } 30 | ) 31 | return context 32 | 33 | def get_formset_factory_kwargs(self): 34 | num_tracks = Track.objects.all().count() 35 | return { 36 | "min_num": num_tracks, 37 | "max_num": num_tracks, 38 | "validate_min": True, 39 | "validate_max": True, 40 | } 41 | 42 | def get_initial(self): 43 | tracks = Track.objects.all().order_by("album__project__artist__name", "name") 44 | return [{"track": track} for track in tracks] 45 | 46 | def formset_valid(self, formset): 47 | for form in formset: 48 | d = form.cleaned_data 49 | track = d["track"] 50 | num_streams = d["streams"] 51 | num_downloads = d["downloads"] 52 | 53 | if num_streams: 54 | ActivityEstimate.objects.create( 55 | activity_type=ActivityEstimate.ACTIVITY_STREAM, 56 | content_object=track, 57 | total=num_streams, 58 | ) 59 | if num_downloads: 60 | ActivityEstimate.objects.create( 61 | activity_type=ActivityEstimate.ACTIVITY_DOWNLOAD, 62 | content_object=track, 63 | total=num_downloads, 64 | ) 65 | 66 | messages.success(self.request, "Daily Report was submitted successfully") 67 | return super().formset_valid(formset) 68 | -------------------------------------------------------------------------------- /music/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MusicConfig(AppConfig): 5 | name = "music" 6 | -------------------------------------------------------------------------------- /music/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.apps import apps 3 | from django.utils.text import slugify 4 | 5 | from campaign.factories import ProjectFactory 6 | from music.models import ActivityEstimate as ActivityEstimateConst 7 | 8 | 9 | class AlbumFactory(factory.django.DjangoModelFactory): 10 | class Meta: 11 | model = apps.get_model("music", "Album") 12 | 13 | project = factory.SubFactory(ProjectFactory) 14 | name = factory.Sequence(lambda n: f"Album #{n}") 15 | slug = factory.LazyAttribute(lambda album: slugify(album.name)) 16 | 17 | 18 | class TrackFactory(factory.django.DjangoModelFactory): 19 | class Meta: 20 | model = apps.get_model("music", "Track") 21 | 22 | album = factory.SubFactory(AlbumFactory) 23 | track_number = factory.LazyAttribute( 24 | lambda track: track.album.track_set.count() + 1 25 | ) 26 | 27 | 28 | class ActivityEstimateFactory(factory.django.DjangoModelFactory): 29 | class Meta: 30 | model = apps.get_model("music", "ActivityEstimate") 31 | 32 | activity_type = ActivityEstimateConst.ACTIVITY_STREAM 33 | content_object = factory.SubFactory(TrackFactory) 34 | total = 500 35 | -------------------------------------------------------------------------------- /music/migrations/0002_auto_20160725_0226.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.8 on 2016-07-25 02:26 2 | import django.db.models.deletion 3 | from django.db import migrations, models 4 | 5 | import music.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("music", "0001_initial")] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Audio", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("file", music.models.S3PrivateFileField(upload_to="artist/audio")), 26 | ( 27 | "album", 28 | models.OneToOneField( 29 | on_delete=django.db.models.deletion.CASCADE, to="music.Album" 30 | ), 31 | ), 32 | ], 33 | options={"verbose_name_plural": "Audio"}, 34 | ), 35 | migrations.AlterField( 36 | model_name="marketplaceurl", 37 | name="medium", 38 | field=models.CharField( 39 | choices=[ 40 | ("spotify", "Spotify"), 41 | ("itunes", "iTunes"), 42 | ("apple", "Apple Music"), 43 | ("google", "Google Play"), 44 | ("amazon", "Amazon"), 45 | ("tidal", "Tidal"), 46 | ("youtube", "YouTube"), 47 | ], 48 | help_text="The type of marketplace", 49 | max_length=10, 50 | ), 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /music/migrations/0003_auto_20160730_1802.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.8 on 2016-07-30 18:02 2 | import django.db.models.deletion 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("music", "0002_auto_20160725_0226")] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="AlbumBio", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | auto_created=True, 18 | primary_key=True, 19 | serialize=False, 20 | verbose_name="ID", 21 | ), 22 | ), 23 | ( 24 | "bio", 25 | models.TextField( 26 | 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.' 27 | ), 28 | ), 29 | ], 30 | ), 31 | migrations.AddField( 32 | model_name="album", 33 | name="release_date", 34 | field=models.DateField(blank=True, null=True), 35 | ), 36 | migrations.AddField( 37 | model_name="albumbio", 38 | name="album", 39 | field=models.OneToOneField( 40 | on_delete=django.db.models.deletion.CASCADE, to="music.Album" 41 | ), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /music/migrations/0004_track.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-10 23:12 2 | import django.db.models.deletion 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("music", "0003_auto_20160730_1802")] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Track", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | auto_created=True, 18 | primary_key=True, 19 | serialize=False, 20 | verbose_name="ID", 21 | ), 22 | ), 23 | ("disc_number", models.PositiveSmallIntegerField(default=1)), 24 | ("track_number", models.PositiveSmallIntegerField()), 25 | ("name", models.CharField(max_length=60)), 26 | ("duration", models.DurationField(blank=True, null=True)), 27 | ( 28 | "album", 29 | models.ForeignKey( 30 | on_delete=django.db.models.deletion.CASCADE, to="music.Album" 31 | ), 32 | ), 33 | ], 34 | ) 35 | ] 36 | -------------------------------------------------------------------------------- /music/migrations/0005_auto_20160911_0829.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-11 08:29 2 | from django.db import migrations 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("music", "0004_track")] 8 | 9 | operations = [ 10 | migrations.AlterUniqueTogether( 11 | name="track", unique_together={("album", "disc_number", "track_number")} 12 | ) 13 | ] 14 | -------------------------------------------------------------------------------- /music/migrations/0006_auto_20160920_0724.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.1 on 2016-09-20 07:24 2 | import django.db.models.deletion 3 | import django.utils.timezone 4 | import gfklookupwidget.fields 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("contenttypes", "0002_remove_content_type_name"), 12 | ("music", "0005_auto_20160911_0829"), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="ActivityEstimate", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("date", models.DateField(default=django.utils.timezone.now)), 29 | ( 30 | "activity_type", 31 | models.CharField( 32 | choices=[("stream", "Stream"), ("download", "Download")], 33 | max_length=8, 34 | ), 35 | ), 36 | ("object_id", gfklookupwidget.fields.GfkLookupField()), 37 | ("total", models.PositiveIntegerField()), 38 | ( 39 | "content_type", 40 | models.ForeignKey( 41 | on_delete=django.db.models.deletion.PROTECT, 42 | to="contenttypes.ContentType", 43 | ), 44 | ), 45 | ], 46 | ), 47 | migrations.AlterUniqueTogether( 48 | name="activityestimate", 49 | unique_together={("date", "activity_type", "content_type", "object_id")}, 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /music/migrations/0007_auto_20190908_2201.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-08 22:01 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | import music.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [("music", "0006_auto_20160920_0724")] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="activityestimate", 16 | name="content_type", 17 | field=models.ForeignKey( 18 | limit_choices_to=music.models.activity_content_type_choices, 19 | on_delete=django.db.models.deletion.PROTECT, 20 | to="contenttypes.ContentType", 21 | ), 22 | ) 23 | ] 24 | -------------------------------------------------------------------------------- /music/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/music/migrations/__init__.py -------------------------------------------------------------------------------- /music/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/music/templatetags/__init__.py -------------------------------------------------------------------------------- /music/templatetags/music.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 25 May 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django import template 8 | 9 | register = template.Library() 10 | 11 | 12 | @register.filter 13 | def trackdurationformat(duration): 14 | if duration: 15 | minutes, seconds = divmod(duration.seconds, 60) 16 | return f"{minutes:02d}:{seconds:02d}" 17 | -------------------------------------------------------------------------------- /music/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 24 July 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.shortcuts import get_object_or_404 8 | from django.views.generic import TemplateView 9 | from django.views.generic.list import ListView 10 | 11 | from campaign.models import Investment 12 | from music.models import Album 13 | 14 | 15 | class MusicListView(ListView): 16 | 17 | template_name = "music/music.html" 18 | context_object_name = "albums" 19 | model = Album 20 | 21 | 22 | class AlbumDetailView(TemplateView): 23 | 24 | template_name = "music/album_detail.html" 25 | 26 | def get_context_data(self, **kwargs): 27 | context = super().get_context_data(**kwargs) 28 | 29 | album = get_object_or_404( 30 | Album, 31 | slug=kwargs["album_slug"], 32 | project__artist__slug=kwargs["artist_slug"], 33 | ) 34 | 35 | user = self.request.user 36 | user_is_investor = ( 37 | user.is_authenticated 38 | and Investment.objects.filter( 39 | campaign__project__album=album, 40 | charge__customer__user=user, 41 | charge__paid=True, 42 | charge__refunded=False, 43 | ).exists() 44 | ) 45 | 46 | context.update({"album": album, "user_is_investor": user_is_investor}) 47 | return context 48 | -------------------------------------------------------------------------------- /perdiem/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/perdiem/__init__.py -------------------------------------------------------------------------------- /perdiem/context_processors.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 11 July 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.contrib.sites.models import Site 8 | 9 | 10 | def request(request): 11 | return {"host": Site.objects.get_current().domain} 12 | -------------------------------------------------------------------------------- /perdiem/gunicorn.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | bind = "0.0.0.0:8000" 4 | workers = multiprocessing.cpu_count() * 2 + 1 5 | -------------------------------------------------------------------------------- /perdiem/nginx/investperdiem.com: -------------------------------------------------------------------------------- 1 | server { 2 | server_name investperdiem.com; 3 | return 301 https://www.investperdiem.com$request_uri; 4 | } 5 | 6 | server { 7 | server_name www.investperdiem.com; 8 | 9 | access_log off; 10 | client_max_body_size 50M; 11 | 12 | location / { 13 | proxy_set_header X-Forwarded-Host $server_name; 14 | proxy_set_header X-Real-IP $remote_addr; 15 | proxy_set_header Host $http_host; 16 | 17 | if ($http_x_forwarded_proto != "https") { 18 | rewrite ^(.*)$ https://$server_name$1 permanent; 19 | } 20 | 21 | proxy_pass http://127.0.0.1:8000; 22 | add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"'; 23 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains;"; 24 | } 25 | } 26 | 27 | server { 28 | listen 80 default_server; 29 | server_name _; 30 | 31 | access_log off; 32 | 33 | location /health-check { 34 | proxy_set_header X-Forwarded-Host $server_name; 35 | proxy_set_header X-Real-IP $remote_addr; 36 | proxy_set_header Host $http_host; 37 | 38 | proxy_pass http://127.0.0.1:8000; 39 | add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /perdiem/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | :Created: 5 March 2016 3 | :Author: Lucas Connors 4 | 5 | """ 6 | 7 | from django.apps import apps 8 | from django.db import connection 9 | from django.db.migrations.executor import MigrationExecutor 10 | from django.test import TestCase, override_settings 11 | from pigeon.test import RenderTestCase 12 | 13 | from accounts.factories import UserFactory 14 | 15 | 16 | @override_settings(PASSWORD_HASHERS=("django.contrib.auth.hashers.MD5PasswordHasher",)) 17 | class PerDiemTestCase(RenderTestCase): 18 | 19 | USER_USERNAME = "jsmith" 20 | USER_EMAIL = "jsmith@example.com" 21 | 22 | @classmethod 23 | def setUpTestData(cls): 24 | super().setUpTestData() 25 | cls.user = UserFactory( 26 | username=cls.USER_USERNAME, 27 | email=cls.USER_EMAIL, 28 | is_staff=True, 29 | is_superuser=True, 30 | ) 31 | 32 | def setUp(self): 33 | self.client.login(username=self.USER_USERNAME, password=UserFactory._PASSWORD) 34 | 35 | 36 | class MigrationTestCase(TestCase): 37 | """ 38 | Ref: https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/ 39 | """ 40 | 41 | migrate_from = None 42 | migrate_to = None 43 | 44 | @property 45 | def app(self): 46 | return apps.get_containing_app_config(type(self).__module__).name 47 | 48 | def setUp(self): 49 | # Verify that migration_from and migration_to are defined 50 | assertion_error_message = ( 51 | "MigrationTestCase '{test_case_name}' must define migrate_from and migrate_to properties." 52 | ).format(test_case_name=type(self).__name__) 53 | assert self.migrate_from and self.migrate_to, assertion_error_message 54 | 55 | # Init MigrationExecutor 56 | self.migrate_from = [(self.app, self.migrate_from)] 57 | self.migrate_to = [(self.app, self.migrate_to)] 58 | executor = MigrationExecutor(connection) 59 | old_apps = executor.loader.project_state(self.migrate_from).apps 60 | 61 | # Reverse to old migration 62 | executor.migrate(self.migrate_from) 63 | 64 | # Create model instances before migration runs 65 | self.setUpBeforeMigration(old_apps) 66 | 67 | # Run the migration to test 68 | executor = MigrationExecutor(connection) 69 | executor.loader.build_graph() 70 | executor.migrate(self.migrate_to) 71 | self.apps = executor.loader.project_state(self.migrate_to).apps 72 | 73 | def setUpBeforeMigration(self, apps): 74 | pass 75 | 76 | 77 | class HealthCheckWebTestCase(RenderTestCase): 78 | def get200s(self): 79 | return ["/health-check/"] 80 | 81 | 82 | class ExtrasWebTestCase(RenderTestCase): 83 | def get200s(self): 84 | return [ 85 | "/faq/", 86 | "/trust/", 87 | "/terms/", 88 | "/privacy/", 89 | "/contact/", 90 | "/artist-resources/", 91 | "/investor-resources/", 92 | ] 93 | 94 | def testContact(self): 95 | # Login as user 96 | user = UserFactory() 97 | self.client.login(username=user.username, password=UserFactory._PASSWORD) 98 | 99 | # Verify that contact form submits successfully 100 | self.assertResponseRedirects( 101 | "/contact/", 102 | "/contact/thanks", 103 | method="POST", 104 | data={ 105 | "inquiry": "general_inquiry", 106 | "email": "msmith@example.com", 107 | "message": "Hello World!", 108 | }, 109 | ) 110 | -------------------------------------------------------------------------------- /perdiem/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for perdiem project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from dotenv import load_dotenv 13 | 14 | load_dotenv() 15 | 16 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "perdiem.settings") 17 | os.environ.setdefault("DJANGO_CONFIGURATION", "BaseConfig") 18 | 19 | 20 | from configurations.wsgi import get_wsgi_application # isort:skip 21 | 22 | 23 | application = get_wsgi_application() 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "perdiem-django" 3 | version = "0.1.0" 4 | description = "Crowdfunding platform for emerging musicians." 5 | authors = ["Lucas Connors ", "localastronaut"] 6 | license = "ISC" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.8" 10 | Django = "~2.2.10" 11 | gunicorn = "^20.0.4" 12 | psycopg2 = "=2.8.6" 13 | Pillow = "^8.0.1" 14 | social-auth-app-django = "^4.0.0" 15 | python-memcached = "^1.59" 16 | requests = "^2.24.0" 17 | boto3 = "^1.16.9" 18 | django-pigeon = "^0.3.0" 19 | sorl-thumbnail = "^12.6.3" 20 | whitenoise = "^5.2.0" 21 | django-s3-storage = "^0.13.4" 22 | djangorestframework = "^3.12.1" 23 | django-ses = "^1.0.3" 24 | geopy = "^2.0.0" 25 | pinax-stripe = "^4.4.0" 26 | django-templated-email = "^2.3.0" 27 | django-markdown-deux = "^1.0.5" 28 | django-pagedown = "^2.1.2" 29 | django-gfklookupwidget = "^1.0.9" 30 | python-dotenv = "^0.15.0" 31 | sentry-sdk = "^0.19.1" 32 | fabric = "^2.5" 33 | django-configurations = "^2.2" 34 | 35 | [tool.poetry.dev-dependencies] 36 | factory-boy = "^3.1.0" 37 | coverage = "^5.3" 38 | black = "^20.8b1" 39 | pyupgrade = "^2.7" 40 | isort = "^5.6.4" 41 | 42 | [tool.isort] 43 | profile = "black" 44 | 45 | [tool.black] 46 | line-length = 88 47 | target_version = ["py38"] 48 | 49 | [build-system] 50 | requires = ["poetry>=1.0"] 51 | build-backend = "poetry.masonry.api" 52 | -------------------------------------------------------------------------------- /static/css/admin/music/daily-report.css: -------------------------------------------------------------------------------- 1 | label { 2 | margin-right: 5px; 3 | } 4 | 5 | input { 6 | margin-right: 15px; 7 | } 8 | 9 | hr { 10 | margin-top: 10px; 11 | margin-bottom: 10px; 12 | } 13 | -------------------------------------------------------------------------------- /static/css/artist/artist_list.css: -------------------------------------------------------------------------------- 1 | .getting-location { 2 | display: none; 3 | } 4 | 5 | .filter-sort-options > ul { 6 | display: inline-block; 7 | text-align: center; 8 | } 9 | 10 | .artist-list-name { 11 | color: #000; 12 | padding-top: 10px; 13 | text-transform: uppercase; 14 | font-family: Dosis, sans-serif; 15 | } 16 | 17 | .filter-sort-options .menu > li { 18 | padding: 0.2em 0.5em; 19 | } 20 | .filter-sort-options .menu > li > a { 21 | padding: 0; 22 | } 23 | .filter-sort-options h5 { 24 | display: inline-block; 25 | font-size: 1.5em; 26 | text-transform: uppercase; 27 | margin: 2px; 28 | padding: 4px; 29 | } 30 | .filter-sort-options h6 { 31 | text-transform: uppercase; 32 | } 33 | 34 | .location { 35 | height: auto; 36 | width: auto; 37 | padding: 6px; 38 | padding-bottom: 0px; 39 | } 40 | 41 | .artistlist { 42 | margin: 20px; 43 | padding: 10px; 44 | max-width: 100%; 45 | } 46 | 47 | .artistlist.row > .column { 48 | background: #fff; 49 | padding: 10px; 50 | border: 1px solid #fff; 51 | text-align: center; 52 | } 53 | 54 | .artistlist.row > .column > p { 55 | color: #000; 56 | text-transform: uppercase; 57 | } 58 | 59 | .break { 60 | padding-left: 5%; 61 | padding-right: 5%; 62 | } 63 | 64 | .column { 65 | color: #ffffff; 66 | } 67 | 68 | .list-nav { 69 | margin-left: 5%; 70 | margin-right: 5%; 71 | padding: 5px; 72 | } 73 | 74 | .nav-float { 75 | float: left; 76 | } 77 | 78 | .artist-details { 79 | font-family: Dosis, sans-serif; 80 | margin: 0px; 81 | } 82 | 83 | .artist-name { 84 | padding-top: 5px; 85 | padding-bottom: 0px; 86 | } 87 | 88 | .artist-location { 89 | padding-top: 5px; 90 | padding-bottom: 10px; 91 | } 92 | 93 | .artist-invested { 94 | padding-top: 5px; 95 | padding-bottom: 5px; 96 | } 97 | 98 | .artist-earned { 99 | padding-top: 0px; 100 | padding-bottom: 10px; 101 | font-weight: 400; 102 | } 103 | 104 | .artist-list-image { 105 | position: relative; 106 | } 107 | 108 | h2.artist-list-overlay { 109 | position: absolute; 110 | margin-bottom: 10px; 111 | bottom: 10%; 112 | width: 100%; 113 | max-width: 250px; 114 | font-size: 25px; 115 | color: #fff; 116 | left: 0; 117 | right: 0; 118 | margin-left: auto; 119 | margin-right: auto; 120 | } 121 | 122 | .banner-active { 123 | background-color: #4A96AD; 124 | } 125 | 126 | .banner-funded { 127 | background-color: #22bb5b; 128 | } 129 | 130 | .banner-coming { 131 | background-color: #34495e; 132 | } 133 | 134 | .button.hollow.location { 135 | border: none; 136 | padding: 0px; 137 | font-size: 1em; 138 | } 139 | 140 | .list-font { 141 | color: #2199e8; 142 | } 143 | -------------------------------------------------------------------------------- /static/css/artist/artist_preview.css: -------------------------------------------------------------------------------- 1 | .artistlist { 2 | margin: 20px; 3 | padding: 50px; 4 | max-width: 100%; 5 | } 6 | 7 | .artistlist.row > .column { 8 | background: #fff; 9 | padding: 20px; 10 | opacity: 0.8; 11 | border: 1px solid #ffffff; 12 | text-align: center; 13 | } 14 | 15 | .artistlist.row > .column > p { 16 | color: #fff; 17 | } 18 | 19 | .column { 20 | color: #ffffff; 21 | } 22 | 23 | .profile-boxes { 24 | text-align: center; 25 | margin: 20px; 26 | } 27 | 28 | .box-margin { 29 | margin: 10px; 30 | padding: 15px; 31 | width: 120px; 32 | background-color: #4A96AD; 33 | } 34 | 35 | .view-artists-box { 36 | width: 200px; 37 | font-size: 1.5em; 38 | font-family: Dosis; 39 | font-weight: 200 40 | } 41 | 42 | .artistlist { 43 | margin: 20px; 44 | padding: 50px; 45 | max-width: 100%; 46 | } 47 | -------------------------------------------------------------------------------- /static/css/extra/resources.css: -------------------------------------------------------------------------------- 1 | .resources-container { 2 | padding-left: 10%; 3 | padding-right: 10%; 4 | max-width: 1000px; 5 | margin: 0 auto; 6 | } 7 | 8 | .img-resources { 9 | height: auto; 10 | max-height: 500px; 11 | max-width: 500px; 12 | box-shadow: 1px 1px 1px 1px grey; 13 | margin: 10px; 14 | } 15 | 16 | .list-copy { 17 | color: #000; 18 | font-family: Roboto; 19 | font-size: .8em; 20 | font-weight: 200; 21 | } 22 | -------------------------------------------------------------------------------- /static/css/extra/stats.css: -------------------------------------------------------------------------------- 1 | .leaderboard-padding { 2 | width: 20%; 3 | background-color: #fff; 4 | height: 125px; 5 | } 6 | .leaderboard-padding:hover { 7 | background-color: #fff; 8 | } 9 | 10 | .chart-name { 11 | font-size: 15px; 12 | padding: 0px; 13 | margin: 0px; 14 | color: black; 15 | } 16 | 17 | .leaderboard-table { 18 | text-align: center; 19 | margin: 20px; 20 | display: inline-block; 21 | float: center; 22 | vertical-align: top; 23 | } 24 | 25 | .total-box { 26 | background-color: #4D698E; 27 | text-align: center; 28 | padding: 5px; 29 | margin: 0 auto; 30 | } 31 | 32 | .chart-amount { 33 | font-size: 15px; 34 | color: #4cbd7e; 35 | padding: 0px; 36 | margin: 0px; 37 | font-weight: 400; 38 | } 39 | 40 | .leaderboard { 41 | margin: 10px auto; 42 | max-width: 800px; 43 | } 44 | -------------------------------------------------------------------------------- /static/css/home.css: -------------------------------------------------------------------------------- 1 | .home-page { 2 | min-height: 100vh; 3 | } 4 | 5 | .about { 6 | min-height: 800px; 7 | background-color: white; 8 | padding-top: 5%; 9 | padding-left: 10%; 10 | padding-right: 10%; 11 | } 12 | 13 | h2.name-header { 14 | padding-top: 40px; 15 | padding-bottom: 50px; 16 | text-align: center; 17 | font-size: 35px; 18 | font-weight: 200; 19 | } 20 | 21 | .footer-position { 22 | position: absolute; 23 | bottom: 10%; 24 | left: 0; 25 | width: 100%; 26 | margin: 0px; 27 | padding: 0px; 28 | text-align: center; 29 | } 30 | 31 | .auth { 32 | bottom: 40%; 33 | } 34 | 35 | .social-icon-home { 36 | margin: 0px; 37 | padding: 0px; 38 | } 39 | 40 | ul.color-white { 41 | color: white; 42 | font-weight: 200; 43 | } 44 | 45 | .title { 46 | font-weight: 200; 47 | padding: 20px; 48 | } 49 | 50 | li.price { 51 | font-size: 20px; 52 | text-transform: uppercase; 53 | font-weight: 400; 54 | } 55 | 56 | 57 | h2.color-black:hover { 58 | color: #85929E; 59 | } 60 | 61 | .sign-up { 62 | background-color: #2199e8; 63 | font-family: Roboto; 64 | } 65 | 66 | a.learn-more { 67 | color: #fff; 68 | 69 | } 70 | 71 | a.learn-more:hover { 72 | color: #85929E; 73 | } 74 | 75 | .home-copy { 76 | text-transform: uppercase; 77 | color: #4d96ad; 78 | font-family: Dosis; 79 | font-size: 20px; 80 | font-weight: 400; 81 | padding: 10px; 82 | } 83 | 84 | .home-button { 85 | margin: 50px; 86 | padding: 20px; 87 | font-family: Dosis; 88 | font-weight: 400; 89 | text-transform: uppercase; 90 | font-size: 18px; 91 | border-radius: 5px; 92 | } 93 | 94 | .home-icon { 95 | color: white; 96 | padding: 5px; 97 | } 98 | 99 | .money-icon { 100 | color: #4cbd7e; 101 | padding: 5px; 102 | } 103 | 104 | .home-description { 105 | padding-top: 5%; 106 | } 107 | 108 | .home-logo-about { 109 | width: 80px; 110 | margin-top: 30px; 111 | } 112 | 113 | .header-padding { 114 | padding-left: 20px; 115 | padding-right: 20px; 116 | } 117 | 118 | .description-padding { 119 | padding-top: 5px; 120 | padding-bottom: 5px; 121 | } 122 | 123 | .header-logo { 124 | width: 400px; 125 | } 126 | 127 | .header-text { 128 | text-shadow: 2px 2px #000; 129 | color: #4cbd7e; 130 | font-size: 3em; 131 | } 132 | 133 | .sub-header-text { 134 | color: white; 135 | font-size: 2em; 136 | } 137 | 138 | .header-position { 139 | padding-top: 20%; 140 | padding-left: 10%; 141 | padding-right: 10%; 142 | } 143 | 144 | .img-phone { 145 | height: auto; 146 | max-height: 500px; 147 | } 148 | .about-padding { 149 | padding: 20%; 150 | } 151 | 152 | #column-wrapper { 153 | max-width: 1000px; 154 | margin: 0 auto; 155 | } 156 | 157 | #leftcolumn, #rightcolumn { 158 | float: left; 159 | max-width: 400px; 160 | padding: 20px; 161 | width: auto; 162 | } 163 | 164 | .description-padding { 165 | padding-top: 30px; 166 | max-width: 500px; 167 | width: 90%; 168 | margin: 0 auto; 169 | } 170 | 171 | .portfolio-padding { 172 | padding-bottom: 20px; 173 | max-width: 500px; 174 | width: 90%; 175 | margin: 0 auto; 176 | } 177 | 178 | .youtube-responsive-container { 179 | position: relative; 180 | padding-bottom: 56.25%; 181 | padding-top: 30px; 182 | height: 0; 183 | overflow: hidden; 184 | } 185 | 186 | .youtube-responsive-container iframe, .youtube-responsive-container object, .youtube-responsive-container embed { 187 | position: absolute; 188 | top: 0; 189 | left: 0; 190 | width: 100%; 191 | height: 100%; 192 | } 193 | -------------------------------------------------------------------------------- /static/css/music/music.css: -------------------------------------------------------------------------------- 1 | .investor-download { 2 | font-size: 20px; 3 | } 4 | 5 | .marketplace-icons { 6 | height: 40px; 7 | padding: 5px; 8 | } 9 | 10 | .album-bio { 11 | margin: 0 auto; 12 | padding-top: 10px; 13 | padding-left: 10px; 14 | padding-right: 10px; 15 | max-width: 600px; 16 | } 17 | 18 | .album-bio > ol { 19 | text-align: center; 20 | list-style-position: inside; 21 | } 22 | 23 | .marketplace-container { 24 | text-align: center; 25 | padding: 20px; 26 | height: 100px; 27 | } 28 | 29 | .download-button { 30 | text-align: center; 31 | background-color: #4A96AD; 32 | width: 150px; 33 | margin: 0 auto; 34 | padding: 10px; 35 | color: #fff; 36 | } 37 | 38 | .download-container { 39 | text-align: center; 40 | padding: 20px; 41 | } 42 | 43 | .chart-container { 44 | margin: 0 auto; 45 | width: 90%; 46 | max-width: 800px; 47 | } 48 | -------------------------------------------------------------------------------- /static/css/profile/profile.css: -------------------------------------------------------------------------------- 1 | .profile-name { 2 | color: #4d96ad; 3 | font-size: 16px; 4 | text-transform: uppercase; 5 | } 6 | 7 | .profile-stats { 8 | margin: 0 auto; 9 | } 10 | 11 | .profile-container { 12 | width: 80%; 13 | margin: 15px auto; 14 | } 15 | 16 | .profile-earned { 17 | font-size: 20px; 18 | color: #4d96ad; 19 | } 20 | 21 | .profile-invested { 22 | font-size: 20px; 23 | color: #4d96ad; 24 | } 25 | 26 | .profile-padding { 27 | padding-left: 10px; 28 | padding-right: 10px; 29 | } 30 | 31 | .canvas-profile { 32 | text-align: center; 33 | padding: 3em; 34 | } 35 | 36 | .public-profile-name { 37 | padding: 5px; 38 | font-weight: 400; 39 | color: #285e6f; 40 | } 41 | 42 | .public-profile-crop { 43 | height: 150px; 44 | width: 150px; 45 | border-radius: 150px; 46 | text-align: center; 47 | margin: 10px; 48 | } 49 | 50 | h2.profile-header { 51 | text-align: center; 52 | font-size: 25px; 53 | padding: 10px; 54 | } 55 | 56 | p.profile-total { 57 | text-align: center; 58 | font-size: 18px; 59 | padding-top: 5px; 60 | padding-left: 20px; 61 | padding-right: 20px; 62 | font-family: Dosis, sans-serif; 63 | } 64 | 65 | h5.profile-total-header { 66 | text-align: center; 67 | font-size: 25px; 68 | padding-top: 5px; 69 | padding-left: 20px; 70 | padding-right: 20px; 71 | } 72 | 73 | h5.profile-total { 74 | text-align: center; 75 | font-size: 25px; 76 | padding: 0px; 77 | font-weight: 400; 78 | } 79 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/favicon.png -------------------------------------------------------------------------------- /static/img/graph.svg: -------------------------------------------------------------------------------- 1 | graph -------------------------------------------------------------------------------- /static/img/icons/perdiem_icon_114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/icons/perdiem_icon_114.png -------------------------------------------------------------------------------- /static/img/icons/perdiem_icon_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/icons/perdiem_icon_144.png -------------------------------------------------------------------------------- /static/img/icons/perdiem_icon_57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/icons/perdiem_icon_57.png -------------------------------------------------------------------------------- /static/img/icons/perdiem_icon_72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/icons/perdiem_icon_72.png -------------------------------------------------------------------------------- /static/img/marketplace/amazon.svg: -------------------------------------------------------------------------------- 1 | amazon -------------------------------------------------------------------------------- /static/img/marketplace/apple.svg: -------------------------------------------------------------------------------- 1 | apple -------------------------------------------------------------------------------- /static/img/marketplace/google.svg: -------------------------------------------------------------------------------- 1 | google -------------------------------------------------------------------------------- /static/img/marketplace/itunes.svg: -------------------------------------------------------------------------------- 1 | itunes -------------------------------------------------------------------------------- /static/img/marketplace/spotify.svg: -------------------------------------------------------------------------------- 1 | spotify -------------------------------------------------------------------------------- /static/img/marketplace/tidal.svg: -------------------------------------------------------------------------------- 1 | tidal -------------------------------------------------------------------------------- /static/img/marketplace/youtube.svg: -------------------------------------------------------------------------------- 1 | youtube -------------------------------------------------------------------------------- /static/img/monkeygif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/monkeygif.gif -------------------------------------------------------------------------------- /static/img/perdiem-all-devices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/perdiem-all-devices.png -------------------------------------------------------------------------------- /static/img/perdiem-anonymous-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/perdiem-anonymous-avatar.png -------------------------------------------------------------------------------- /static/img/perdiem-background-profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/perdiem-background-profile.jpg -------------------------------------------------------------------------------- /static/img/perdiem-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/perdiem-background.jpg -------------------------------------------------------------------------------- /static/img/perdiem-earnings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/perdiem-earnings.png -------------------------------------------------------------------------------- /static/img/perdiem-invest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/perdiem-invest.png -------------------------------------------------------------------------------- /static/img/perdiem-small.svg: -------------------------------------------------------------------------------- 1 | perdiem-small -------------------------------------------------------------------------------- /static/img/pie.svg: -------------------------------------------------------------------------------- 1 | pie -------------------------------------------------------------------------------- /static/img/resources/perdiem-anonymous.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/resources/perdiem-anonymous.jpg -------------------------------------------------------------------------------- /static/img/resources/perdiem-avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/resources/perdiem-avatar.jpg -------------------------------------------------------------------------------- /static/img/resources/perdiem-calculator.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/resources/perdiem-calculator.jpg -------------------------------------------------------------------------------- /static/img/resources/perdiem-cashout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/resources/perdiem-cashout.jpg -------------------------------------------------------------------------------- /static/img/resources/perdiem-earnings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/resources/perdiem-earnings.jpg -------------------------------------------------------------------------------- /static/img/resources/perdiem-emails.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/resources/perdiem-emails.jpg -------------------------------------------------------------------------------- /static/img/resources/perdiem-media.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/resources/perdiem-media.jpg -------------------------------------------------------------------------------- /static/img/resources/perdiem-register.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/resources/perdiem-register.jpg -------------------------------------------------------------------------------- /static/img/resources/perdiem-updates.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/resources/perdiem-updates.jpg -------------------------------------------------------------------------------- /static/img/social-icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RevolutionTech/perdiem-django/ca3415514969745583f56585465dbbe481eda282/static/img/social-icon.jpg -------------------------------------------------------------------------------- /static/js/accounts/login-errors.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 'use strict' 3 | 4 | $('#login-modal').foundation('open'); 5 | }); 6 | -------------------------------------------------------------------------------- /static/js/artist/artist-list.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 'use strict' 3 | 4 | // Close location dropdown pane when clicking away 5 | $(document).click(function() { 6 | $('#location-dropdown').foundation('close'); 7 | }); 8 | $('.dropdown a').click(function() { 9 | $('#location-dropdown').foundation('close'); 10 | }); 11 | $('.dropdown-pane').click(function(e) { 12 | e.stopPropagation(); 13 | }); 14 | $('button#location-dropdown-button').click(function(e) { 15 | $('#location-dropdown').foundation('toggle'); 16 | e.stopPropagation(); 17 | }); 18 | 19 | // Reset location 20 | $('.dropdown-pane button#location-reset-button').click(function() { 21 | window.location.href = '?genre=' + active_genre + '&sort=' + order_by; 22 | }); 23 | 24 | // Set my location 25 | $('.dropdown-pane #my-location button').click(function() { 26 | my_location = false; 27 | $('.dropdown-pane #my-location').hide(); 28 | $('.dropdown-pane #text-location').show(); 29 | }); 30 | $('.dropdown-pane #text-location button').click(function() { 31 | my_location = true; 32 | $('.dropdown-pane #text-location').hide(); 33 | $('.dropdown-pane #my-location').show(); 34 | }); 35 | 36 | // Filter by user's location 37 | function get_order_by_location(position) { 38 | var lat, lon; 39 | if (position && position.coords) { 40 | lat = position.coords.latitude; 41 | lon = position.coords.longitude; 42 | } 43 | var url = '?genre=' + active_genre + '&distance=' + $('.dropdown-pane input').val(); 44 | if (lat) url += '&lat=' + lat; 45 | if (lon) url += '&lon=' + lon; 46 | url += '&sort=' + order_by; 47 | window.location.href = url; 48 | } 49 | $('.dropdown-pane button#location-update-button').click(function() { 50 | if (my_location) { 51 | if (navigator.geolocation) { 52 | $('.getting-location').show(); 53 | navigator.geolocation.getCurrentPosition(get_order_by_location, get_order_by_location); 54 | } else { 55 | get_order_by_location(null); 56 | } 57 | } else { 58 | window.location.href = '?genre=' + active_genre + '&distance=' + $('.dropdown-pane input').val() + '&location=' + $('.dropdown-pane #text-location input').val() + '&sort=' + order_by; 59 | } 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /static/js/vendor/jquery.cookie.min.js: -------------------------------------------------------------------------------- 1 | /*! jquery.cookie v1.4.1 | MIT */ 2 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?a(require("jquery")):a(jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(g," ")),h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(void 0!==g&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;o>n;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0===a.cookie(b)?!1:(a.cookie(b,"",a.extend({},c,{expires:-1})),!a.cookie(b))}}); -------------------------------------------------------------------------------- /static/js/vendor/smooth-scroll.js: -------------------------------------------------------------------------------- 1 | /* Source: http://www.learningjquery.com/2007/10/improved-animated-scrolling-script-for-same-page-links */ 2 | 3 | $(document).ready(function() { 4 | function filterPath(string) { 5 | return string 6 | .replace(/^\//,'') 7 | .replace(/(index|default).[a-zA-Z]{3,4}$/,'') 8 | .replace(/\/$/,''); 9 | } 10 | var locationPath = filterPath(location.pathname); 11 | var scrollElem = scrollableElement('html', 'body'); 12 | 13 | $('a[href*=#]').each(function() { 14 | var thisPath = filterPath(this.pathname) || locationPath; 15 | if ( locationPath == thisPath 16 | && (location.hostname == this.hostname || !this.hostname) 17 | && this.hash.replace(/#/,'') ) { 18 | var $target = $(this.hash), target = this.hash; 19 | if (target) { 20 | var targetOffset = $target.offset().top; 21 | $(this).click(function(event) { 22 | event.preventDefault(); 23 | $(scrollElem).animate({scrollTop: targetOffset}, 400, function() { 24 | location.hash = target; 25 | }); 26 | }); 27 | } 28 | } 29 | }); 30 | 31 | // use the first element that is "scrollable" 32 | function scrollableElement(els) { 33 | for (var i = 0, argLength = arguments.length; i 0) { 37 | return el; 38 | } else { 39 | $scrollElement.scrollTop(1); 40 | var isScrollable = $scrollElement.scrollTop()> 0; 41 | $scrollElement.scrollTop(0); 42 | if (isScrollable) { 43 | return el; 44 | } 45 | } 46 | } 47 | return []; 48 | } 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /static/js/widgets/coordinates.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 'use strict' 3 | 4 | $('#id_coordinates').click(function() { 5 | $.getJSON("/api/coordinates/", { address: $('#id_location').val() }) 6 | .done(function(resp) { 7 | // Update latitude and longitude 8 | $('#id_lat').val(resp.latitude); 9 | $('#id_lon').val(resp.longitude); 10 | }) 11 | .fail(function(resp) { 12 | // Show user error message 13 | console.log(resp.responseText); 14 | alert(resp.responseText); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block content %} 5 |

404

6 | 7 |

CAN'T FIND THIS PAGE

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block content %} 5 |

500

6 | 7 |

OOPS! SOMETHING WENT WRONG

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/admin/music/activityestimate/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | 3 | {% block object-tools-items %} 4 |
  • 5 | Enter Daily Report 6 |
  • 7 | {{ block.super }} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/admin/music/activityestimate/daily-report.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load static %} 3 | 4 | {% block extrastyle %} 5 | 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

    Note: activity estimates will be entered in for {% now "DATE_FORMAT" %}.

    10 | 11 |
    12 | {% csrf_token %} 13 | {{ formset.non_form_errors }} 14 | {{ formset.management_form }} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for form in formset %} 25 | {{ form.non_field_errors }} 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% endfor %} 33 |
    ArtistTrack# Streams# Downloads
    {{ form.track.initial.album.project.artist.name }}{{ form.track.initial.name }}{{ form.track }}{{ form.streams.errors }}{{ form.streams }}{{ form.downloads.errors }}{{ form.downloads }}
    34 | 35 | 36 |
    37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /templates/artist/artist_application.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Create An Artist Account

    5 |
    6 |
    7 |
    8 |
    Want to learn more before signing up? Check out our Artist Resources page for more details
    9 |
    10 |
    11 |
    12 |
    13 |
    14 | {% csrf_token %} 15 | {{ form.as_p }} 16 |

    17 | I understand that PerDiem will exclusively distribute the music project (free of charge) for the payback period listed in this form. All royalties (100%) will be split between the artist(s) and investors based on the artist(s)' terms. 18 |

    19 | 20 |
    21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/artist/artist_application_thanks.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Artist Application

    5 |
    6 |

    Thank you! Your submission was received successfully.

    7 |

    We will be contacting you soon with further details on launching your campaign. If you haven't already, please look over the ARTIST RESOURCES page and REACH OUT TO US if you have any additional questions.

    8 |
    9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /templates/artist/includes/artist_current_project.html: -------------------------------------------------------------------------------- 1 | {% load humanize %} 2 | {% load perdiem %} 3 | 4 |

    Current Project

    5 |
    6 | 7 |
    8 |
      9 | {% if user_investor %} 10 |
    • 11 | My percentage: 12 | {% if user_investor.percentage < 0.005 %} 13 | <0.01% 14 | {% else %} 15 | {{ user_investor.percentage|notrail_floatformat:2 }}% 16 | {% endif %} 17 |
    • 18 | {% endif %} 19 | {% for percentage_breakdown in campaign.project.artist_percentage %} 20 |
    • {{ percentage_breakdown.name }}: {{ percentage_breakdown.percentage|notrail_floatformat:2 }}%
    • 21 | {% endfor %} 22 |
    • 23 | {% if user_investor %}Other Investors:{% else %}Investors:{% endif %} 24 | {{ fans_percentage|notrail_floatformat:2 }}% 25 |
    • 26 |
    27 | -------------------------------------------------------------------------------- /templates/artist/includes/artist_detail_invest.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | {% load humanize %} 3 | {% load perdiem %} 4 | 5 |
    6 | {% if user.is_authenticated %} 7 |

    Invest in {{ campaign.project.reason }}

    8 |
    9 |
    10 |
    11 | {% include "artist/includes/invest.html" %} 12 | {% else %} 13 |

    To Invest in {{ artist.name }}

    14 | {% include "artist/includes/auth_buttons.html" %} 15 | {% endif %} 16 | 17 |
    18 |
    19 |
    20 | 21 |

    CURRENT INVESTORS

    22 | 38 | 39 |
    40 | -------------------------------------------------------------------------------- /templates/artist/includes/artist_detail_overview.html: -------------------------------------------------------------------------------- 1 | {% load humanize %} 2 | {% load thumbnail %} 3 | {% load markdown_deux_tags %} 4 | {% load perdiem %} 5 | 6 |
    7 | 8 | 9 | {% thumbnail artist.photo.img "500x500" as thumb %} 10 |
    11 | {{ artist.name }} 12 |
    13 | {% endthumbnail %} 14 | 15 | 16 | {% with genres=artist.genres.all %} 17 |

    18 | {{ genres|length|pluralize }} {{ genres|join:" / " }} 19 | // {{ artist.location }} 20 |

    21 | {% endwith %} 22 | 23 | {% if artist.playlist_set.exists %} 24 |
    25 | {% for playlist in artist.playlist_set.all %} 26 | {{ playlist.html|safe }} 27 | {% endfor %} 28 |
    29 | {% endif %} 30 | 31 |
    32 |
    33 |
    34 | 35 | 36 | {% if artist.bio %} 37 |
    38 | {{ artist.bio.bio|markdown:'trusted' }} 39 |
    40 | {% endif %} 41 | 42 | 43 | {% for social in artist.social_set.all %} 44 | 45 | {% endfor %} 46 |
    47 |
    48 |
    49 |
    50 | -------------------------------------------------------------------------------- /templates/artist/includes/artist_detail_projects.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | {% load humanize %} 3 | {% load thumbnail %} 4 | {% load markdown_deux_tags %} 5 | {% load perdiem %} 6 | {% load music %} 7 | 8 |
    9 | {% if campaign %} 10 | {% include "artist/includes/artist_current_project.html" %} 11 | {% endif %} 12 | {% if latest_campaign and latest_campaign.project.generated_revenue %} 13 |
    14 |
    15 | ${{ latest_campaign.project.generated_revenue|notrail_floatformat:2|intcomma }} 16 | generated so far from this project 17 | (${{ latest_campaign.project.generated_revenue_fans|notrail_floatformat:2|intcomma }} 18 | for investors). 19 |
    20 |
    21 |
    22 |
    23 | {% endif %} 24 | {% include "artist/includes/artist_past_campaigns.html" %} 25 |
    26 | -------------------------------------------------------------------------------- /templates/artist/includes/artist_detail_updates.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if user.is_authenticated %} 3 | {% if updates or has_permission_to_submit_update %} 4 |

    LATEST HAPPENINGS

    5 | {% include "artist/includes/artist_updates.html" with updates=updates has_permission_to_submit_update=has_permission_to_submit_update %} 6 | {% if has_permission_to_submit_update %} 7 |

    POST AN UPDATE

    8 |
    9 | {% csrf_token %} 10 | 14 | 15 |
    16 |
    17 |

    18 | {{ form.image.errors }} 19 | {{ form.image.label }}: 20 | {{ form.image }} 21 |

    22 |
    23 |
    24 |

    25 | {{ form.youtube_url.errors }} 26 | {{ form.youtube_url.label }}: 27 | {{ form.youtube_url }} 28 |

    29 |
    30 |

    {{ form.non_field_errors }}

    31 |

    32 | {{ form.title.errors }} 33 | {{ form.title.label }}: 34 | {{ form.title }} 35 |

    36 |

    37 | {{ form.text.errors }} 38 | {{ form.text.label }}: 39 | {{ form.text }} 40 |

    41 |
    42 | 43 |
    44 | {% endif %} 45 | {% else %} 46 |

    This is where artists will share their updates.

    47 |

    Looks like they haven't posted yet, but when they do you'll see them right here

    48 | {% endif %} 49 | {% else %} 50 |

    To View Updates

    51 | {% include "artist/includes/auth_buttons.html" %} 52 | {% endif %} 53 |
    54 | -------------------------------------------------------------------------------- /templates/artist/includes/artist_past_campaigns.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | {% load humanize %} 3 | {% load perdiem %} 4 | 5 |

    Past Campaigns

    6 |
      7 | {% for past_campaign in artist.past_campaigns %} 8 |
      9 |
      10 |
      11 |
      12 |
    • 13 |
      14 |
      {{ past_campaign.project.reason }}
      15 |
      16 |
      Raised ${{ past_campaign.amount|intcomma }} for {{ past_campaign.fans_percentage }}% of {{ past_campaign.project.reason }} revenue
      17 |
      {{ past_campaign.use_of_funds }}
      18 | {% if past_campaign.project.generated_revenue %} 19 |
      ${{ past_campaign.project.generated_revenue|notrail_floatformat:2|intcomma }} generated so far from this project
      20 |
      ${{ past_campaign.project.generated_revenue_fans|notrail_floatformat:2|intcomma }} for investors
      21 | {% endif %} 22 |
      23 |
      24 |
      25 |
    • 26 |
      27 | {% endfor %} 28 |
    29 | -------------------------------------------------------------------------------- /templates/artist/includes/artist_update_media.html: -------------------------------------------------------------------------------- 1 | {% load thumbnail %} 2 | 3 | {% for media_url in update.updatemediaurl_set.all %} 4 | {% if not forloop.first %}
    {% endif %} 5 | {% if embed %} 6 | {{ media_url.embed_html|safe }} 7 | {% else %} 8 | {{ media_url.thumbnail_html|safe }} 9 | {% endif %} 10 | {% endfor %} 11 | {% for image in update.updateimage_set.all %} 12 | {% if not forloop.first %}
    {% endif %} 13 | {% thumbnail image.img "500x500" as thumb %} 14 | {{ update.title }} 15 | {% endthumbnail %} 16 | {% endfor %} 17 | -------------------------------------------------------------------------------- /templates/artist/includes/artist_updates.html: -------------------------------------------------------------------------------- 1 | {% load markdown_deux_tags %} 2 | 3 |
    4 | 25 |
    26 | -------------------------------------------------------------------------------- /templates/artist/includes/auth_buttons.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /templates/artist/includes/invest.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | {% load humanize %} 3 | {% load perdiem %} 4 | 5 | {% if latest_campaign %} 6 | {% with campaign=latest_campaign %} 7 | {% if campaign.open %} 8 | {% if request.user.is_authenticated %} 9 |
    10 |
    {{ campaign.project.reason }}
    11 |
    12 |
    {{ campaign.num_shares_remaining }} shares remaining
    13 | 14 |
    15 | 16 | 17 | 18 |
    19 |
    Number of Shares
    20 |
    Total Cost:
    21 |
    22 |
    23 |
    24 |
    {{ artist.name }} is raising ${{ campaign.amount|intcomma }} and giving {{ campaign.fans_percentage }}% of sales from {{ campaign.project.reason }} to investors
    25 |
    The value of a share is ${{ campaign.value_per_share }}
    26 |
    Each share is worth {{ campaign.percentage_per_share|notrail_floatformat:2 }}% of {{ campaign.project.reason }} revenue
    27 |
    This project is valued at ${{ campaign.valuation }}
    28 | {% else %} 29 | {% include "artist/includes/auth_buttons.html" %} 30 |

    To invest in {{ artist.name }}

    31 | {% endif %} 32 | {% else %} 33 | {% if campaign.percentage_funded == 100 %} 34 |

    Sold Out

    35 | {% endif %} 36 |

    This campaign is no longer available {{ campaign.end_datetime|date:'F jS' }}

    37 | {% endif %} 38 | {% endwith %} 39 | {% endif %} 40 | -------------------------------------------------------------------------------- /templates/artist/includes/investor_profile_artist_list.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | {% load humanize %} 3 | 4 | {% block extrastyle %} 5 | 6 | {% endblock %} 7 | 8 |
    9 | {% for artist in artists.values %} 10 |
    11 |
    12 | {% include "object_preview.html" with object=artist img=artist.photo.img %} 13 |
    14 | {% if show_earnings %} 15 |
    16 |

    17 | {% if artist.total_earned %} 18 | 19 | {% endif %} 20 | ${{ artist.total_earned|floatformat:2|intcomma }} / 21 | ${{ artist.total_invested|floatformat:2|intcomma }} 22 |

    23 |
    24 | {% endif %} 25 |
    26 | {% endfor %} 27 |
    28 | -------------------------------------------------------------------------------- /templates/artist/includes/share_buttons.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | Email 5 | 6 | 7 | 8 | 9 | Facebook 10 | 11 | 12 | 13 | 14 | Google 15 | 16 | 17 | 18 | 19 | LinkedIn 20 | 21 | 22 | 23 | 24 | Twitter 25 | 26 |
    27 | -------------------------------------------------------------------------------- /templates/email/artist_apply.email: -------------------------------------------------------------------------------- 1 | {% extends "email/base.email" %} 2 | 3 | {% block email_subject %}Artist Application: {{ artist_name }}{% endblock %} 4 | 5 | {% block plain_body %} 6 | Artist Info: 7 | {% if user_id %} 8 | User ID: {{ user_id }} 9 | {% endif %} 10 | Artist / Band Name: {{ artist_name }} 11 | Photo Link: {{ photo_link }} 12 | Genre: {{ genre }} 13 | Location: {{ location }} 14 | Bio: {{ bio }} 15 | Email: {{ email }} 16 | Phone number: {{ phone_number }} 17 | 18 | Campaign Details: 19 | Project: {{ project }} 20 | Reason: {{ campaign_reason }} 21 | Amount Raising: {{ amount_raising }} 22 | Giving Back: {{ giving_back }} 23 | Start Date: {{ campaign_start }} 24 | End Date: {{ campaign_end }} 25 | Payback Period: {{ payback_period }} 26 | 27 | Social: 28 | SoundCloud: {{ soundcloud }} 29 | {% if spotify %} 30 | Spotify: {{ spotify }} 31 | {% endif %} 32 | {% if facebook %} 33 | Facebook: {{ facebook }} 34 | {% endif %} 35 | {% if twitter %} 36 | Twitter: {{ twitter }} 37 | {% endif %} 38 | {% if instagram %} 39 | Instagram: {{ instagram }} 40 | {% endif %} 41 | {% endblock %} 42 | 43 | {% block html_body %} 44 | Artist Info 45 |
      46 | {% if user_id %} 47 |
    • User ID: {{ user_id }}
    • 48 | {% endif %} 49 |
    • Artist / Band Name: {{ artist_name }}
    • 50 |
    • Photo Link: {{ photo_link }}
    • 51 |
    • Genre: {{ genre }}
    • 52 |
    • Location: {{ location }}
    • 53 |
    • Bio: {{ bio }}
    • 54 |
    • Email: {{ email }}
    • 55 |
    • Phone number: {{ phone_number }}
    • 56 |
    57 | 58 | Campaign Details 59 |
      60 |
    • Project: {{ project }}
    • 61 |
    • Reason: {{ campaign_reason }}
    • 62 |
    • Amount Raising: {{ amount_raising }}
    • 63 |
    • Giving Back: {{ giving_back }}
    • 64 |
    • Start Date: {{ campaign_start }}
    • 65 |
    • End Date: {{ campaign_end }}
    • 66 |
    • Payback Period: {{ payback_period }}
    • 67 |
    68 | 69 | Social: 70 |
      71 |
    • SoundCloud {{ soundcloud }}
    • 72 | {% if spotify %} 73 |
    • Spotify: {{ spotify }}
    • 74 | {% endif %} 75 | {% if facebook %} 76 |
    • Facebook: {{ facebook }}
    • 77 | {% endif %} 78 | {% if twitter %} 79 |
    • Twitter: {{ twitter }}
    • 80 | {% endif %} 81 | {% if instagram %} 82 |
    • Instagram: {{ instagram }}
    • 83 | {% endif %} 84 |
    85 | {% endblock %} 86 | -------------------------------------------------------------------------------- /templates/email/artist_update.email: -------------------------------------------------------------------------------- 1 | {% extends "email/base.email" %} 2 | {% load markdown_deux_tags %} 3 | 4 | {% block email_subject %}{{ artist.name }}: {{ update.title|safe }}{% endblock %} 5 | 6 | {% block plain_body %} 7 | {{ update.text|markdown|striptags }} 8 | 9 | View the full update on the artist's page on PerDiem: {{ host }}{% url 'artist' slug=artist.slug %}. 10 | {% endblock %} 11 | 12 | {% block html_body %} 13 | {% include "artist/includes/artist_update_media.html" with update=update embed=False %} 14 | {{ update.text|markdown }} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/email/base.email: -------------------------------------------------------------------------------- 1 | {% block subject %}{% block email_subject %}A message from PerDiem{% endblock %}{% endblock %} 2 | 3 | {% block plain %} 4 | {% block plain_body %}{% endblock %} 5 | {{ unsubscribe_message.plain }} 6 | {% endblock %} 7 | 8 | {% block html %} 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | {% block html_body %}{% endblock %} 20 |

    21 | {{ unsubscribe_message.html|safe }} 22 |

    23 | 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/email/contact.email: -------------------------------------------------------------------------------- 1 | {% extends "email/base.email" %} 2 | 3 | {% block email_subject %}{{ inquiry }}{% endblock %} 4 | 5 | {% block plain_body %} 6 | {% if user_id %} 7 | User ID: {{ user_id }} 8 | {% endif %} 9 | Email: {{ email }} 10 | {% if first_name or last_name %} 11 | Name: {{ first_name }}{% if first_name and last_name %} {% endif %}{{ last_name }} 12 | {% endif %} 13 | Message: {{ message }} 14 | {% endblock %} 15 | 16 | {% block html_body %} 17 | {% if user_id %} 18 |

    User ID: {{ user_id }}

    19 | {% endif %} 20 |

    Email: {{ email }}

    21 | {% if first_name or last_name %} 22 |

    Name: {{ first_name }}{% if first_name and last_name %} {% endif %}{{ last_name }}

    23 | {% endif %} 24 |

    Message: {{ message }}

    25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/email/email_verification.email: -------------------------------------------------------------------------------- 1 | {% extends "email/base.email" %} 2 | 3 | {% block email_subject %}Verify Your Email{% endblock %} 4 | 5 | {% block plain_body %} 6 | It looks like you changed your email address. Please go to the following link to verify your email address: 7 | {{ host }}{{ verify_email_url }} 8 | {% endblock %} 9 | 10 | {% block html_body %} 11 |

    It looks like you changed your email address. Please click here to verify your email address.

    12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/email/invest_success.email: -------------------------------------------------------------------------------- 1 | {% extends "email/base.email" %} 2 | 3 | {% block email_subject %}Thank You for investing in {{ artist.name }}!{% endblock %} 4 | 5 | {% block plain_body %} 6 | Thank you for investing in {{ artist.name }}. You are now the owner of {{ num_shares }} shares in {{ artist.name }}'s campaign {{ campaign.project.reason }}. You'll be able to see how this project is doing along with updates from the artist directly from your profile. 7 | 8 | You will also be able to listen to the music before anyone else and download exclusive + unreleased tracks from your artists in the "My Music" section of your profile. 9 | 10 | Need help finding your profile? After logging in, click on the avatar in the top right corner of the site and you're in. 11 | 12 | If you have any questions please e-mail us at: support@investperdiem.com 13 | {% endblock %} 14 | 15 | {% block html_body %} 16 |

    Thank you for investing in {{ artist.name }}. You are now the owner of {{ num_shares }} shares in {{ artist.name }}'s campaign {{ campaign.project.reason }}. You'll be able to see how this project is doing along with updates from the artist on your profile.

    17 |

    You will also be able to listen to the music before anyone else and download exclusive + unreleased tracks from your artists in the "My Music" section of your profile.

    18 |

    If you have any questions please e-mail us at: support@investperdiem.com.

    19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/email/welcome.email: -------------------------------------------------------------------------------- 1 | {% extends "email/base.email" %} 2 | 3 | {% block email_subject %}Welcome to PerDiem!{% endblock %} 4 | 5 | {% block plain_body %} 6 | Thanks for your interest in supporting a new generation of artists and contributing to the progression of their careers. We're excited to have you on board! 7 | 8 | {% if verify_email_url %} 9 | Please go to the following link to verify your email address: 10 | {{ host }}{{ verify_email_url }} 11 | {% endif %} 12 | 13 | You are now able to discover and invest in emerging artists through the platform. Please read over our FAQ and For Investors to get a better understanding of how investing works. We're constantly making updates and improving the site so be sure to subscribe and stay updated with all the latest info. 14 | {{ host }}{% url 'artists' %} 15 | {% endblock %} 16 | 17 | {% block html_body %} 18 |

    Thanks for your interest in supporting a new generation of artists and contributing to the progression of their careers. We're excited to have you on board!

    19 | 20 | {% if verify_email_url %} 21 |

    Please click here to verify your email address.

    22 | {% endif %} 23 |

    You are now able to discover and invest in emerging artists through the platform. Please read over our FAQ and For Investors to get a better understanding of how investing works. We're constantly making updates and improving the site so be sure to subscribe and stay updated with all the latest info.

    24 |

    Discover artists on PerDiem.

    25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/extra/funding.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    How Much Should I Raise?

    5 |
    6 |

    I want to raise ($)

    7 | 8 |

    Percentage to Investors

    9 | 21 |

    Amount needed to break even

    22 |

    23 | $ 24 |

    25 |

    Estimated streams Needed

    26 |

    27 |
    28 |
    29 |
    30 | Calculate 31 |
    32 | {% endblock %} 33 | 34 | {% block extrajs %} 35 | 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /templates/extra/trust.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Trust & Safety

    5 |
    6 |
    7 |
    8 |
    9 |
    Our mission at PerDiem is to strengthen the relationship between artists and fans
    10 |
    For Artists
    11 |

    We don't make artists sign contracts because we want to give them complete freedom and ownership of their music.

    12 |

    This means that an artist can leave at anytime.

    13 |

    Which means we must always act in the best interest for artists.

    14 |

    When you choose to be part of the platform, you are agreeing to produce the record that your fans invested in and release it through PerDiem.

    15 |

    This means that you get to create the record that you want.

    16 |

    This also means that your fans are going to be looking forward to the release of your album.

    17 |
    For Investors
    18 |

    As a fan, you are the loud speaker for the artist.

    19 |

    When artists do good and treat their fans well, you guys share the love and tell your friends how amazing they are.

    20 |

    The system is designed to protect both the artists and the fans.

    21 |

    By distributing the music, we are easily able to track revenue and divide it to the appropriate places.

    22 |

    There is a chance that you may not make a return.

    23 |

    There is always a risk in investing, so you should only invest in artists that you believe in.

    24 |
    WE RESPECT ARTISTS AND THE CREATIVE COMMUNITY
    25 |
    OUR GOAL IS TO CREATE A NEW WAY TO MAKE IT SUSTAINABLE
    26 |
    27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/leaderboard/leaderboard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load staticfiles %} 4 | {% load humanize %} 5 | {% load perdiem %} 6 | 7 | {% block extrastyle %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

    Top Investors

    13 |
    14 |
    15 |
    16 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/music/music.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block extrastyle %} 5 | 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
    11 |

    Music

    12 |
    13 |
    14 |
    15 |

    RELEASES AND EXCLUSIVES ON PERDIEM

    16 |
    17 | {% for album in albums %} 18 |
    19 |
    20 | {% include "object_preview_music.html" with object=album img=album.artwork.img %} 21 |
    22 |
    23 | {% endfor %} 24 |
    25 |
    26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/object_preview.html: -------------------------------------------------------------------------------- 1 | {% load thumbnail %} 2 | 3 | 13 | -------------------------------------------------------------------------------- /templates/object_preview_music.html: -------------------------------------------------------------------------------- 1 | {% extends "object_preview.html" %} 2 | 3 | {% block text %} 4 | {{ object.name }}
    by {{ object.project.artist.name }}
    5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/registration/contact.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
    5 |

    Contact Form

    6 |
    7 |
    8 |
    9 |
    10 |
    11 | {% csrf_token %} 12 | {{ form.as_p }} 13 | 14 |
    15 |
    16 |
    17 |
    18 |
    19 |

    Are you an artist? Interested in being on PerDiem?

    20 |
    21 | Sign Up 22 |
    23 |
    24 |
    25 |
    26 |
    27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/registration/contact_thanks.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Contact

    5 |
    6 |

    Thank you! Your message was received successfully.

    7 |
    8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/registration/error/account-does-not-exist.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Error: Account Does Not Exist

    5 |
    6 |

    It looks like you're trying to login to PerDiem with an email address we do not have in our records. Please try a different email address or trying registering a new account instead.

    7 |
    8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/registration/error/account-exists.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Error: Account Already Exists

    5 |
    6 |

    It looks like you're trying to register a new account on PerDiem, but an account already exists with the email you are trying to authenticate with. Please try logging in instead.

    7 |
    8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/registration/error/email-required.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Error: Email Required

    5 |
    6 |

    Sorry! You cannot create a PerDiem account without an email address. Please try again with a different account or sign up the old-fashioned way.

    7 |
    8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/registration/includes/profile_earnings.html: -------------------------------------------------------------------------------- 1 | {% load humanize %} 2 | 3 |
    4 |

    ${{ total_earned|floatformat:2|intcomma }}

    5 |

    Return on Investment

    6 |

    {{ percentage|floatformat:2|intcomma }}% | ${{ total_investments|floatformat:2|intcomma }}

    7 |
    8 |
    9 |
    10 |
    11 | 12 |
    13 |
    14 | -------------------------------------------------------------------------------- /templates/registration/includes/profile_music.html: -------------------------------------------------------------------------------- 1 | {% load humanize %} 2 | 3 |
    4 |

    My Music

    5 |
    6 |
    7 |
    8 |
    9 | {% for album in albums %} 10 |
    11 |
    12 | {% include "object_preview_music.html" with object=album img=album.artwork.img %} 13 |
    14 |
    15 |

    16 | {% if album.total_streams %} 17 | 18 | {{ album.total_streams|intcomma }} 19 | {% endif %} 20 | {% if album.total_downloads %} 21 | 22 | {{ album.total_downloads|intcomma }} 23 | {% endif %} 24 | {% if not album.total_streams and not album.total_downloads %} 25 | EXCLUSIVE 26 | {% endif %} 27 |

    28 |
    29 |
    30 | {% endfor %} 31 |
    32 |
    33 | -------------------------------------------------------------------------------- /templates/registration/includes/profile_portfolio.html: -------------------------------------------------------------------------------- 1 | {% load humanize %} 2 | 3 |
    4 |

    My Artists

    5 |
    6 |
    7 |
    8 | {% include "artist/includes/investor_profile_artist_list.html" with artists=artists show_earnings=True %} 9 |
    10 | -------------------------------------------------------------------------------- /templates/registration/includes/settings_avatar_form.html: -------------------------------------------------------------------------------- 1 | {% extends "registration/includes/settings_form.html" %} 2 | 3 | {% block form_inner_html %} 4 | {{ form.non_field_errors }} 5 |

    Select one of your existing avatars:

    6 | {{ form.avatar.errors }} 7 | {% for radio in form.avatar %} 8 | 19 | {% endfor %} 20 |

    Or upload a new one (2MB maximum size):

    21 | {{ form.custom_avatar.errors }} 22 | {{ form.custom_avatar }} 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/registration/includes/settings_form.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {% csrf_token %} 4 | 5 | {% block form_inner_html %}{{ form.as_p }}{% endblock %} 6 | 7 |
    8 |
    9 | -------------------------------------------------------------------------------- /templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Password Reset

    5 |
    6 |

    Your password has been reset. You may go ahead and log in now.

    7 |
    8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Password Reset

    5 |
    6 | {% if form %} 7 |
    8 | {% csrf_token %} 9 | {{ form.as_p }} 10 | 11 |
    12 | {% else %} 13 |

    This password reset link is invalid. Please be sure this link matches the one from the email we sent you exactly.

    14 | {% endif %} 15 |
    16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Password Reset

    5 |
    6 |

    We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly.

    7 |

    If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder.

    8 |
    9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Password Reset

    5 |
    6 |
    7 | {% csrf_token %} 8 | {{ form.as_p }} 9 | 10 |
    11 |
    12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/registration/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load staticfiles %} 3 | {% load humanize %} 4 | 5 | {% block extrastyle %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |

    {% firstof user.get_full_name user.get_username %}

    12 | {% if user.userprofile.invest_anonymously %}

    (displays publicly as Anonymous)

    {% endif %} 13 | 14 | {% if artists %} 15 | 21 | 22 |
    23 |
    24 | {% if updates %} 25 | {% include "artist/includes/artist_updates.html" with updates=updates show_artist_name=True %} 26 | {% else %} 27 |

    This is where you will receive updates from your artists.

    28 |

    Looks like they haven't posted yet, but when they do it'll feed right here.

    29 | {% endif %} 30 |
    31 |
    32 | {% include "registration/includes/profile_portfolio.html" %} 33 |
    34 |
    35 | {% include "registration/includes/profile_music.html" %} 36 |
    37 |
    38 | {% include "registration/includes/profile_earnings.html" %} 39 |
    40 |
    41 | {% else %} 42 |

    You haven't invested in anything yet!

    43 |
    44 | START INVESTING 45 |
    46 | {% endif %} 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /templates/registration/public_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load staticfiles %} 3 | {% load humanize %} 4 | 5 | {% block extrastyle %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
    12 |
    Profile 13 |

    {{ profile_user.userprofile.get_display_name }}

    14 |
    15 |
    16 |

    ${{ profile.total_earned|floatformat:2|intcomma }}

    17 |
    18 |
    19 |
    20 |
    21 | {% if profile.artists %} 22 |
    23 |

    PORTFOLIO

    24 | {% include "artist/includes/investor_profile_artist_list.html" with artists=profile.artists %} 25 |
    26 |
    27 |
    28 |
    29 | {% else %} 30 |

    {{ profile_user.userprofile.get_display_name }} hasn't invested in anything yet!

    31 | {% endif %} 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /templates/registration/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block content %} 5 |
    6 |

    Register

    7 |
    8 |
    9 |
    10 |
    11 | 18 | 25 |
    26 | {% csrf_token %} 27 |

    28 | Username and password are case-sensitive 29 |

    30 | {{ form.as_p }} 31 |

    32 | By clicking register, you agree to the Terms & Conditions 33 |

    34 | 35 |
    36 |
    37 |
    38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /templates/registration/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Settings

    5 | 11 |
    12 |
    13 |
    14 |
    15 | {% include "registration/includes/settings_form.html" with form_name='edit_name' tab_name='name' form=edit_name_form is_default_form=True %} 16 | {% include "registration/includes/settings_avatar_form.html" with form_name='edit_avatar' tab_name='avatar' includes_files=True form=edit_avatar_form %} 17 | {% include "registration/includes/settings_form.html" with form_name='change_password' tab_name='password' form=change_password_form %} 18 | {% include "registration/includes/settings_form.html" with form_name='email_preferences' tab_name='email-preferences' form=email_preferences_form %} 19 |
    20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /templates/registration/unsubscribe.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Unsubscribed

    5 |
    6 | {% if success %} 7 |

    The email address {{ user.email }} has been unsuccessfully unsubscribed from {{ subscription_type_display }} on PerDiem.

    8 | {% else %} 9 |

    This link is invalid or has expired. Please try a link from a more recent email or sign in and try again.

    10 | {% endif %} 11 |
    12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/registration/verify_email.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

    Email Verified

    5 |
    6 |

    The email address {{ verified_email.email }} has been successfully verified.

    7 |
    8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/widgets/coordinates.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | --------------------------------------------------------------------------------