diff --git a/.travis.yml b/.travis.yml index 986b66f0..f20881a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,14 +8,17 @@ cache: - node_modules services: - postgresql +env: + - DJANGO_SETTINGS_MODULE=modernomad.settings.local + DATABASE_URL=postgres://postgres@localhost/test_db install: - "pip install -U pip wheel" - "nvm install 8" - "pip install -r requirements.txt -r requirements.test.txt" before_script: + - psql -c 'create database test_db;' -U postgres - "cd client && npm install && cd .." - "cd client && ./node_modules/.bin/webpack --config webpack.prod.config.js && cd .." - - "cp modernomad/local_settings.travis.py modernomad/local_settings.py" script: ./manage.py test notifications: slack: diff --git a/Dockerfile b/Dockerfile index 4770dad9..8af1d64a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:2-alpine +FROM python:3.6.6-alpine3.8 # So Pillow can find zlib ENV LIBRARY_PATH /lib:/usr/lib @@ -38,8 +38,7 @@ RUN cd client && node_modules/.bin/webpack --config webpack.prod.config.js # Set configuration last so we can change this without rebuilding the whole # image -ENV DJANGO_SETTINGS_MODULE modernomad.settings_docker -ENV MODE PRODUCTION +ENV DJANGO_SETTINGS_MODULE modernomad.settings.production # Number of gunicorn workers ENV WEB_CONCURRENCY 3 EXPOSE 8000 diff --git a/api/tests/commands/test_data_generation.py b/api/tests/commands/test_data_generation.py new file mode 100644 index 00000000..c3e893a1 --- /dev/null +++ b/api/tests/commands/test_data_generation.py @@ -0,0 +1,8 @@ +from django.test import TransactionTestCase +from django.core.management import call_command + + +class DataGenerationTest(TransactionTestCase): + + def test_run_data_generation(self): + call_command('generate_test_data') diff --git a/app.json b/app.json new file mode 100644 index 00000000..eb6c8e14 --- /dev/null +++ b/app.json @@ -0,0 +1,30 @@ +{ + "name": "modernomad", + "scripts": { + }, + "env": { + "ALLOWED_HOSTS": ".herokuapp.com", + "AWS_ACCESS_KEY_ID": { + "required": true + }, + "AWS_SECRET_ACCESS_KEY": { + "required": true + }, + "AWS_STORAGE_BUCKET_NAME": { + "required": true + }, + "SECRET_KEY": { + "generator": "secret" + } + }, + "formation": { + "worker": { + "quantity": 1 + }, + "web": { + "quantity": 1 + } + }, + "addons": ["cloudamqp", "papertrail", "heroku-postgresql"], + "buildpacks": [] +} diff --git a/core/factory_apps/__init__.py b/core/factory_apps/__init__.py new file mode 100644 index 00000000..cd065649 --- /dev/null +++ b/core/factory_apps/__init__.py @@ -0,0 +1,28 @@ +from faker import Faker +from faker.providers import lorem +from faker.providers import profile +from faker.providers import address +from faker.providers import python +from faker.providers import date_time +from faker.providers import misc +from faker.providers import BaseProvider +import factory + +factory.Faker.add_provider(misc) +factory.Faker.add_provider(date_time) +factory.Faker.add_provider(python) +factory.Faker.add_provider(lorem) +factory.Faker.add_provider(profile) +factory.Faker.add_provider(address) + + +class Provider(BaseProvider): + # Note that the class name _must_ be ``Provider``. + def slug(self, name): + fake = Faker() + value = getattr(fake, name)() + return value.replace(' ', '-') + + +factory.Faker.add_provider(Provider) + diff --git a/core/factory_apps/accounts.py b/core/factory_apps/accounts.py new file mode 100644 index 00000000..b1c271e3 --- /dev/null +++ b/core/factory_apps/accounts.py @@ -0,0 +1,9 @@ +from . import factory + + +class HouseAccountFactory(factory.DjangoModelFactory): + pass + + +class UseTransactionFactory(factory.DjangoModelFactory): + pass diff --git a/core/factory_apps/communication.py b/core/factory_apps/communication.py new file mode 100644 index 00000000..87fa2438 --- /dev/null +++ b/core/factory_apps/communication.py @@ -0,0 +1,15 @@ +from . import factory +from core import models +from .user import UserFactory + + +class EmailtemplateFactory(factory.DjangoModelFactory): + class Meta: + model = models.EmailTemplate + + body = factory.Faker('paragraph') + subject = factory.Faker('words') + name = factory.Faker('words') + creator = factory.SubFactory(UserFactory) + shared = factory.Faker('pybool') + context = models.EmailTemplate.BOOKING diff --git a/core/factory_apps/events.py b/core/factory_apps/events.py new file mode 100644 index 00000000..00a043b1 --- /dev/null +++ b/core/factory_apps/events.py @@ -0,0 +1,119 @@ +from . import factory +from .location import LocationFactory + +from gather.models import EventAdminGroup +from gather.models import EventSeries +from gather.models import Event +from gather.models import EventNotifications + +from .user import UserFactory + + +class EventAdminGroupFactory(factory.DjangoModelFactory): + class Meta: + model = EventAdminGroup + + location = factory.SubFactory(LocationFactory) + + @factory.post_generation + def users(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + # A list of groups were passed in, use them + for user in extracted: + self.users.add(user) + + +class EventSeriesFactory(factory.DjangoModelFactory): + class Meta: + model = EventSeries + + name = factory.Faker('word') + description = factory.Faker('paragraph') + + +class EventFactory(factory.DjangoModelFactory): + class Meta: + model = Event + + created = factory.Faker('past_datetime') + updated = factory.Faker('past_datetime') + start = factory.Faker('future_datetime') + end = factory.Faker('future_datetime') + + title = factory.Faker('words') + slug = factory.Faker('words') + + description = factory.Faker('paragraph') + image = factory.django.ImageField(color='gray') + + notifications = factory.Faker('pybool') + + where = factory.Faker('city') + creator = factory.SubFactory(UserFactory) + + organizer_notes = factory.Faker('paragraph') + + limit = factory.Faker('random_digit') + visibility = Event.PUBLIC + status = Event.PENDING + + location = factory.SubFactory(LocationFactory) + series = factory.SubFactory(EventSeriesFactory) + admin = factory.SubFactory(EventAdminGroupFactory) + + @factory.post_generation + def attendees(self, create, extracted, **kwargs): + if not create: + return + + if extracted: + for users in extracted: + self.attendees.add(users) + + @factory.post_generation + def organizers(self, create, extracted, **kwargs): + if not create: + return + + if extracted: + for users in extracted: + self.organizers.add(users) + + @factory.post_generation + def endorsements(self, create, extracted, **kwargs): + if not create: + return + + if extracted: + for users in extracted: + self.endorsements.add(users) + + +class EventNotificationFactory(factory.DjangoModelFactory): + class Meta: + model = EventNotifications + + user = factory.SubFactory(UserFactory) + reminders = factory.Faker('pybool') + + @factory.post_generation + def location_weekly(self, create, extracted, **kwargs): + if not create: + return + + if extracted: + for location in extracted: + self.location_weekly.add(location) + + @factory.post_generation + def location_publish(self, create, extracted, **kwargs): + if not create: + return + + if extracted: + for location in extracted: + self.location_publish.add(location) diff --git a/core/factory_apps/location.py b/core/factory_apps/location.py new file mode 100644 index 00000000..bfa2322b --- /dev/null +++ b/core/factory_apps/location.py @@ -0,0 +1,148 @@ +from django.contrib.flatpages.models import FlatPage + +from core.models import Location +from core.models import Resource +from core.models import LocationFee +from core.models import LocationMenu +from core.models import LocationFlatPage +from core.models import LocationEmailTemplate +from core.models import CapacityChange +from core.models import Fee +from . import factory + + +class FeeFactory(factory.DjangoModelFactory): + class Meta: + model = Fee + + description = factory.Faker('text') + percentage = factory.Faker('pyfloat', left_digits=0, positive=True) + paid_by_house = factory.Faker('pybool') + + +class LocationFactory(factory.DjangoModelFactory): + class Meta: + model = Location + django_get_or_create = ('slug',) + + name = factory.Faker('street_name') + slug = factory.Faker('slug', name='street_name') + short_description = factory.Faker('text') + address = factory.Faker('street_address') + image = factory.django.ImageField(color='blue') + profile_image = factory.django.ImageField(color='red') + latitude = factory.Faker('latitude') + longitude = factory.Faker('longitude') + + welcome_email_days_ahead = factory.Faker('random_int') + max_booking_days = factory.Faker('random_int') + + stay_page = factory.Faker('text') + front_page_stay = factory.Faker('text') + front_page_participate = factory.Faker('text') + announcement = factory.Faker('text') + + house_access_code = factory.Faker('word') + ssid = factory.Faker('word') + ssid_password = factory.Faker('word') + + timezone = factory.Faker('word') + bank_account_number = factory.Faker('random_int') + routing_number = factory.Faker('random_int') + + bank_name = factory.Faker('word') + name_on_account = factory.Faker('word') + email_subject_prefix = factory.Faker('word') + + check_out = factory.Faker('word') + check_in = factory.Faker('word') + visibility = factory.Iterator(['public', 'members', 'link']) + + @factory.post_generation + def house_admins(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + # A list of groups were passed in, use them + for user in extracted: + self.house_admins.add(user) + + @factory.post_generation + def readonly_admins(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + # A list of groups were passed in, use them + for user in extracted: + self.readonly_admins.add(user) + + +class ResourceFactory(factory.DjangoModelFactory): + class Meta: + model = Resource + + name = factory.Faker('name') + location = factory.SubFactory(LocationFactory) + default_rate = factory.Faker('pydecimal', left_digits=0, positive=True) + description = factory.Faker('text') + summary = factory.Faker('sentence') + cancellation_policy = factory.Faker('text') + image = factory.django.ImageField(color='green') + + +class LocationFeeFactory(factory.DjangoModelFactory): + class Meta: + model = LocationFee + + location = factory.SubFactory(LocationFactory) + fee = factory.SubFactory(FeeFactory) + + +class LocationMenuFactory(factory.DjangoModelFactory): + class Meta: + model = LocationMenu + + location = factory.SubFactory(LocationFactory) + name = factory.Faker('text', max_nb_chars=15) + + +class FlatpageFactory(factory.DjangoModelFactory): + class Meta: + model = FlatPage + + +class LocationFlatPageFactory(factory.DjangoModelFactory): + class Meta: + model = LocationFlatPage + + menu = factory.SubFactory(LocationMenuFactory) + flatpage = factory.SubFactory(FlatpageFactory) + + +class LocationEmailTemplateFactory(factory.DjangoModelFactory): + class Meta: + model = LocationEmailTemplate + + location = factory.SubFactory(LocationFactory) + key = 'admin_daily_update' + text_body = factory.Faker('text') + html_body = factory.Faker('text') + + +class CapacityChangeFactory(factory.DjangoModelFactory): + class Meta: + model = CapacityChange + + created = factory.Faker('past_datetime') + resource = factory.SubFactory(ResourceFactory) + start_date = factory.Faker('future_date') + quantity = factory.Faker('pyint') + accept_drft = factory.Faker('pybool') + + +class BackingFactory(factory.DjangoModelFactory): + pass diff --git a/core/factory_apps/payment.py b/core/factory_apps/payment.py new file mode 100644 index 00000000..b28b9715 --- /dev/null +++ b/core/factory_apps/payment.py @@ -0,0 +1,138 @@ +from . import factory +from core import models + +from .user import UserFactory +from .location import LocationFactory +from .location import ResourceFactory +from .location import FeeFactory + + +class SubscriptionFactory(factory.DjangoModelFactory): + class Meta: + model = models.Subscription + + created = factory.Faker('past_datetime') + updated = factory.Faker('past_datetime') + + created_by = factory.SubFactory(UserFactory) + location = factory.SubFactory(LocationFactory) + user = factory.SubFactory(UserFactory) + + price = factory.Faker('pydecimal', left_digits=3, positive=True) + description = factory.Faker('words') + + start_date = factory.Faker('future_date') + end_date = factory.Faker('future_date') + + +class BillFactory(factory.DjangoModelFactory): + '''Bookings, BillLineItem or Subscription''' + class Meta: + model = models.Bill + + generated_on = factory.Faker('past_datetime') + comment = factory.Faker('paragraph') + + +class SubscriptionBillFactory(factory.DjangoModelFactory): + class Meta: + model = models.SubscriptionBill + + generated_on = factory.Faker('past_datetime') + comment = factory.Faker('paragraph') + + period_start = factory.Faker('future_date') + period_end = factory.Faker('future_date') + subscription = factory.SubFactory(SubscriptionFactory) + + +class BookingBillFactory(factory.DjangoModelFactory): + class Meta: + model = models.BookingBill + + generated_on = factory.Faker('past_datetime') + comment = factory.Faker('paragraph') + + +class BillLineItem(factory.DjangoModelFactory): + class Meta: + model = models.BillLineItem + + bill = factory.SubFactory(BillFactory) + fee = factory.SubFactory(FeeFactory) + + description = factory.Faker('words') + amount = factory.Faker('pydecimal', left_digits=3, positive=True) + paid_by_house = factory.Faker('pybool') + custom = factory.Faker('pybool') + + +class UseFactory(factory.DjangoModelFactory): + class Meta: + model = models.Use + + created = factory.Faker('past_datetime') + updated = factory.Faker('past_datetime') + + location = factory.SubFactory(LocationFactory) + user = factory.SubFactory(UserFactory) + resource = factory.SubFactory(ResourceFactory) + + status = models.Use.PENDING + arrive = factory.Faker('future_date') + depart = factory.Faker('future_date') + arrival_time = factory.Faker('words') + purpose = factory.Faker('paragraph') + last_msg = factory.Faker('past_datetime') + accounted_by = models.Use.FIAT + + +class BookingFactory(factory.DjangoModelFactory): + # deprecated fields not modeled in factory + class Meta: + model = models.Booking + + created = factory.Faker('past_datetime') + updated = factory.Faker('past_datetime') + + comments = factory.Faker('paragraph') + rate = factory.Faker('pydecimal', left_digits=3, positive=True) + uuid = factory.Faker('uuid4') + + bill = factory.SubFactory(BookingBillFactory) + use = factory.SubFactory(UseFactory) + + @factory.post_generation + def suppressed_fees(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + # A list of groups were passed in, use them + for fee in extracted: + self.suppressed_fees.add(fee) + + +class PaymentFactory(factory.DjangoModelFactory): + class Meta: + model = models.Payment + + bill = factory.SubFactory(BillFactory) + user = factory.SubFactory(UserFactory) + payment_date = factory.Faker('past_datetime') + + # payment_service and payment_method may be empty so ignoring those + paid_amount = factory.Faker('pydecimal', left_digits=3, positive=True) + transaction_id = factory.Faker('uuid4') + last4 = factory.Faker('pyint') + + +class UseNoteFactory(factory.DjangoModelFactory): + class Meta: + model = models.UseNote + + +class SubscriptionNoteFactory(factory.DjangoModelFactory): + class Meta: + model = models.SubscriptionNote diff --git a/core/factory_apps/user.py b/core/factory_apps/user.py new file mode 100644 index 00000000..53345a2f --- /dev/null +++ b/core/factory_apps/user.py @@ -0,0 +1,26 @@ +from django.contrib.auth import get_user_model + +from . import factory + +User = get_user_model() + + +class UserFactory(factory.DjangoModelFactory): + class Meta: + model = User + + username = factory.Faker('name') + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name') + email = factory.Faker('email') + is_staff = factory.Faker('pybool') + is_active = True + is_superuser = False + + +class UserProfileFactory(factory.DjangoModelFactory): + pass + + +class UserNote(factory.DjangoModelFactory): + pass diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/core/management/commands/generate_test_data.py b/core/management/commands/generate_test_data.py new file mode 100644 index 00000000..559fedfd --- /dev/null +++ b/core/management/commands/generate_test_data.py @@ -0,0 +1,69 @@ +import contextlib +import io + +from django.core.management.base import BaseCommand +from django.core.management import call_command +from django.db import connection + +from faker import Faker + +from core.factory_apps import location +from core.factory_apps import communication +from core.factory_apps import events +from core.factory_apps import payment + + +class Command(BaseCommand): + help = 'Closes the specified poll for voting' + + def handle(self, *args, **options): + f = io.StringIO() + with contextlib.redirect_stdout(f): + call_command('sqlflush') + flush_db = f.getvalue() + + with connection.cursor() as cursor: + statements = flush_db.split('\n') + for statement in statements: + if not statement: + continue + cursor.execute(statement) + + call_command('migrate') + + # setting the seed so output is consistent + fake = Faker() + fake.seed(1) + # Building location specific things + locationobj = location.LocationFactory() + resource = location.ResourceFactory(location=locationobj) + location.LocationFee(location=locationobj) + + menu = location.LocationMenuFactory(location=locationobj) + location.LocationFlatPageFactory(menu=menu) + + location.CapacityChangeFactory(resource=resource) + + # event things + event_admin = events.EventAdminGroupFactory(location=locationobj) + event_series = events.EventSeriesFactory() + events.EventFactory( + location=locationobj, admin=event_admin, series=event_series + ) + events.EventNotificationFactory() + + # communication + communication.EmailtemplateFactory() + + # payment + subscription = payment.SubscriptionFactory(location=locationobj) + payment.SubscriptionBillFactory(subscription=subscription) + + bill = payment.BookingBillFactory() + payment.BillLineItem(bill=bill) + use = payment.UseFactory(location=locationobj, resource=resource) + payment.BookingFactory(bill=bill, use=use) + + payment.PaymentFactory(bill=bill) + + self.stdout.write(self.style.SUCCESS('Successfully generated testdata')) diff --git a/core/models.py b/core/models.py index fb9c3b41..6cba7b8f 100644 --- a/core/models.py +++ b/core/models.py @@ -3,20 +3,15 @@ from dateutil.relativedelta import relativedelta from django.contrib.auth.models import User -from django.core.files.storage import FileSystemStorage from django.db import models -from django.contrib.sites.models import Site -from django.core import urlresolvers from PIL import Image import os import datetime from django.conf import settings from django.core.files.storage import default_storage import uuid -import stripe -from django.db.models import Q, Sum +from django.db.models import Q from decimal import Decimal -from django.utils.safestring import mark_safe import calendar from django.utils import timezone from django.core.urlresolvers import reverse @@ -27,12 +22,7 @@ # imports for signals import django.dispatch from django.dispatch import receiver -from django.db.models.signals import pre_save, post_save, m2m_changed - -# mail imports -from django.core.mail import EmailMultiAlternatives -from django.template.loader import get_template -from django.template import Context +from django.db.models.signals import pre_save, m2m_changed # bank app imports from bank.models import Account, Transaction, Currency @@ -209,7 +199,6 @@ def has_capacity(self, arrive=None, depart=None): return False def events(self, user=None): - today = timezone.localtime(timezone.now()) if 'gather' in settings.INSTALLED_APPS: from gather.models import Event return Event.objects.upcoming(upto=5, current_user=user, location=self) @@ -357,10 +346,11 @@ def formatday(self, day, weekday): class ResourceManager(models.Manager): def backed_by(self, user): - resources = self.get_queryset().filter(backing__money_account__owners = user) + resources = self.get_queryset().filter(backing__money_account__owners=user) print(resources) return resources + class Resource(models.Model): name = models.CharField(max_length=200) location = models.ForeignKey(Location, related_name='resources', null=True) @@ -591,6 +581,7 @@ def set_next_backing(self, backers, new_backing_date): new_backing = Backing.objects.setup_new(resource=self, backers=backers, start=new_backing_date) logger.debug('created new backing %d' % new_backing.id) + class Fee(models.Model): description = models.CharField(max_length=100, verbose_name="Fee Name") percentage = models.FloatField(default=0, help_text="For example 5.2% = 0.052") @@ -1684,24 +1675,24 @@ def accounts_in_currency(self, currency): return list(self.user.accounts_owned.filter(currency=currency)) + list(self.user.accounts_administered.filter(currency=currency)) - User.profile = property(lambda u: UserProfile.objects.get_or_create(user=u)[0]) User.rooms_backed = (lambda u: Resource.objects.backed_by(user=u)) User._meta.ordering = ['username'] + # Note: primary.accounts.'through' is django's name for the M2M class @receiver(m2m_changed, sender=UserProfile.primary_accounts.through) def primary_accounts_changed(sender, action, instance, reverse, pk_set, **kwargs): logger.debug('p2p_changed signal') logger.debug(action) if action == "pre_add": - logger.debug(instance) # should be a UserProfile unless reversed + logger.debug(instance) # should be a UserProfile unless reversed # since the sender is defined as UserProfile in the @receiver line, # UserProfile is the forward relation and Account is the reverse relation - if reverse: # Account.primary_for.add(...) + if reverse: # Account.primary_for.add(...) account = instance - else: # UserProfile.primary_accounts.add(...) + else: # UserProfile.primary_accounts.add(...) user_profile = instance for pk in pk_set: @@ -1718,6 +1709,7 @@ def primary_accounts_changed(sender, action, instance, reverse, pk_set, **kwargs # ensure user is an owner of primary account assert user_profile.user in account.owners.all() + @receiver(pre_save, sender=UserProfile) def size_images(sender, instance, **kwargs): try: @@ -1777,9 +1769,11 @@ class EmailTemplate(models.Model): SUBJECT_PREFIX = settings.EMAIL_SUBJECT_PREFIX FROM_ADDRESS = settings.DEFAULT_FROM_EMAIL + BOOKING = 'booking' + SUBSCRIPTION = 'subscription' context_options = ( - ('booking', 'Booking'), - ('subscription', 'Subscription') + (BOOKING, 'Booking'), + (SUBSCRIPTION, 'Subscription') ) body = models.TextField(verbose_name="The body of the email") @@ -1962,11 +1956,11 @@ def quantity_on(self, date, resource): def would_not_change_previous_quantity(self, capacity): previous_capacity = self._previous_capacity(capacity) - return (previous_capacity and - ( previous_capacity.quantity == capacity.quantity ) - and - (previous_capacity.accept_drft == capacity.accept_drft) - ) + return all([ + previous_capacity, + previous_capacity.quantity == capacity.quantity, + previous_capacity.accept_drft == capacity.accept_drft + ]) def same_as_next_quantity(self, capacity): logger.debug('same_as_next_quantity') @@ -1978,11 +1972,11 @@ def same_as_next_quantity(self, capacity): logger.debug(capacity.quantity) logger.debug(capacity.accept_drft) - return (next_capacity and - ( next_capacity.quantity == capacity.quantity) - and - ( next_capacity.accept_drft == capacity.accept_drft) - ) + return all([ + next_capacity, + next_capacity.quantity == capacity.quantity, + next_capacity.accept_drft == capacity.accept_drft + ]) class CapacityChange(models.Model): @@ -1996,9 +1990,10 @@ class CapacityChange(models.Model): class Meta: unique_together = ('start_date', 'resource',) + class BackingManager(models.Manager): def by_user(self, user): - return self.get_queryset().filter(money_account__owners = user) + return self.get_queryset().filter(money_account__owners=user) def setup_new(self, resource, backers, start): b = Backing(resource=resource, start=start) @@ -2012,6 +2007,7 @@ def setup_new(self, resource, backers, start): b.drft_account.save() return b + class Backing(models.Model): resource = models.ForeignKey(Resource, related_name='backings') money_account = models.ForeignKey(Account, related_name='+') @@ -2056,16 +2052,19 @@ def _setup_accounts(self, backers): assert not hasattr(self, 'money_account') and not hasattr(self, 'drft_account') # create accounts for this backing usd, _ = Currency.objects.get_or_create(name="USD", defaults={'symbol': '$'}) - ma = Account.objects.create(currency=usd, - name="%s Backing USD Account" % self.resource, - type = Account.CREDIT) + ma = Account.objects.create( + currency=usd, + name="%s Backing USD Account" % self.resource, + type=Account.CREDIT + ) ma.owners.add(*backers) self.money_account = ma drft, _ = Currency.objects.get_or_create(name='DRFT', defaults={'symbol': 'Ɖ'}) - da = Account.objects.create(currency=drft, - name="%s Backing DRFT Account" % self.resource, - type = Account.CREDIT) + da = Account.objects.create( + currency=drft, + name="%s Backing DRFT Account" % self.resource, + type=Account.CREDIT) da.owners.add(*backers) self.drft_account = da @@ -2074,6 +2073,7 @@ class HouseAccount(models.Model): location = models.ForeignKey(Location) account = models.ForeignKey(Account) + class UseTransaction(models.Model): use = models.ForeignKey(Use) transaction = models.ForeignKey(Transaction) diff --git a/core/urls/location.py b/core/urls/location.py index f53dd6ff..683d0e79 100644 --- a/core/urls/location.py +++ b/core/urls/location.py @@ -46,5 +46,5 @@ urlpatterns = patterns('core.views.unsorted', url(r'^$', 'location_list', name='location_list'), - url(r'^(?P\w+)/', include(per_location_patterns)), + url(r'^(?P[\w-]+)/', include(per_location_patterns)), ) diff --git a/core/views/booking.py b/core/views/booking.py index 801acb2e..68d8833b 100644 --- a/core/views/booking.py +++ b/core/views/booking.py @@ -3,6 +3,7 @@ from django.conf import settings from django.contrib import messages +from django.contrib.sites.models import Site from django.contrib.auth.decorators import login_required from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseRedirect @@ -20,7 +21,7 @@ from .view_helpers import _get_user_and_perms from core.emails import send_booking_receipt, new_booking_notify from core.forms import BookingUseForm -from core.models import Booking, Use, Location, Site +from core.models import Booking, Use, Location logger = logging.getLogger(__name__) diff --git a/core/views/unsorted.py b/core/views/unsorted.py index 506cc638..d3eddd12 100644 --- a/core/views/unsorted.py +++ b/core/views/unsorted.py @@ -1561,7 +1561,7 @@ def BookingManageAction(request, location_slug, booking_id): days_until_arrival = (booking.use.arrive - datetime.date.today()).days if days_until_arrival <= location.welcome_email_days_ahead: guest_welcome(booking.use) - except CardError, e: + except CardError as e: # raise Booking.ResActionError(e) # messages.add_message(request, messages.INFO, "There was an error: %s" % e) # status_area_html = render(request, "snippets/res_status_area.html", {"r": booking, 'location': location, 'error': True}) @@ -1867,7 +1867,7 @@ def BillCharge(request, location_slug, bill_id): try: payment = payment_gateway.charge_user(user, bill, charge_amount_dollars, reference) - except CardError, e: + except CardError as e: messages.add_message(request, messages.INFO, "Charge failed with the following error: %s" % e) if bill.is_booking_bill(): return HttpResponseRedirect(reverse('booking_manage', args=(location_slug, bill.bookingbill.booking.id))) diff --git a/core/views/use.py b/core/views/use.py index d73d05e3..6c17efde 100644 --- a/core/views/use.py +++ b/core/views/use.py @@ -5,10 +5,11 @@ from django.shortcuts import get_object_or_404 import logging from django.contrib import messages +from django.contrib.sites.models import Site from django.http import HttpResponse, HttpResponseRedirect from django.core.urlresolvers import reverse -from core.models import Use, Location, Site +from core.models import Use, Location @login_required def UseDetail(request, use_id, location_slug): diff --git a/docker-compose.yml b/docker-compose.yml index ba3ebd19..5e8479d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,5 @@ -version: "3.0" +version: "3.4" + services: web: build: . @@ -6,8 +7,7 @@ services: ports: - "8000:8000" environment: - - "SECRET_KEY=insecure - only for development" - - "MODE=DEVELOPMENT" + - "DJANGO_SETTINGS_MODULE=modernomad.settings.local" - "STRIPE_SECRET_KEY" - "STRIPE_PUBLISHABLE_KEY" - "DISCOURSE_BASE_URL" @@ -32,8 +32,7 @@ services: build: . command: bin/celeryd environment: - - "SECRET_KEY=insecure - only for development" - - "MODE=DEVELOPMENT" + - "DJANGO_SETTINGS_MODULE=modernomad.settings.local" - "STRIPE_SECRET_KEY" - "STRIPE_PUBLISHABLE_KEY" - "DISCOURSE_BASE_URL" diff --git a/docs/how-to-run.md b/docs/how-to-run.md index c28c5edd..f2f34592 100644 --- a/docs/how-to-run.md +++ b/docs/how-to-run.md @@ -70,11 +70,12 @@ create your own local_settings.py file from local_settings.example.py. inside th - browse through settings.py. make note of the location of the media directory and media_url, and any other settings of interest. -## initialisation +## Initialisation go back into the top level repository directory and do the following: -- `./manage.py migrate` will initialise the database on first run +- `./manage.py migrate` will initialise the database on first run. In the next step you want to get some initial data into the database. +- `./manage.py generate_test_data`. This will populate the database with some randomized initial data. ## Run! diff --git a/gather/models.py b/gather/models.py index e02c9ce2..c868cad7 100755 --- a/gather/models.py +++ b/gather/models.py @@ -1,13 +1,12 @@ -import uuid, os, datetime +import uuid +import os import logging from django.conf import settings -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import User from django.db import models from django.db.models.signals import post_save from django.utils import timezone -from PIL import Image -import requests from core.models import Location @@ -26,6 +25,7 @@ def __unicode__(self): class Meta: app_label = 'gather' + class EventSeries(models.Model): ''' Events may be associated with a series. A series has a name and desciption, and its own landing page which lists the associated events. ''' @@ -38,6 +38,7 @@ def __unicode__(self): class Meta: app_label = 'gather' + def event_img_upload_to(instance, filename): ext = filename.split('.')[-1] # rename file to random string @@ -49,16 +50,21 @@ def event_img_upload_to(instance, filename): os.makedirs(upload_abs_path) return os.path.join(upload_path, filename) + class EventManager(models.Manager): def upcoming(self, upto=None, current_user=None, location=None): # return the events happening today or in the future, returning up to # the number of events specified in the 'upto' argument. today = timezone.now() logger.debug(today) + qs = super().get_queryset() + upcoming = qs.filter(end__gte=today).exclude( + status=Event.CANCELED + ).order_by('start') - upcoming = super(EventManager, self).get_queryset().filter(end__gte = today).exclude(status=Event.CANCELED).order_by('start') if location: upcoming = upcoming.filter(location=location) + viewable_upcoming = [] for event in upcoming: if event.is_viewable(current_user): @@ -71,7 +77,7 @@ def upcoming(self, upto=None, current_user=None, location=None): class Meta: app_label = 'gather' -# Create your models here. + class Event(models.Model): PENDING = 'waiting for approval' FEEDBACK = 'seeking feedback' @@ -106,15 +112,15 @@ class Event(models.Model): description = models.TextField(help_text="Basic HTML markup is supported for your event description.") image = models.ImageField(upload_to=event_img_upload_to) attendees = models.ManyToManyField(User, related_name="events_attending", blank=True) - notifications = models.BooleanField(default = True) + notifications = models.BooleanField(default=True) # where, site, place, venue - where = models.CharField(verbose_name = 'Where will the event be held?', max_length=500, help_text="Either a specific room at this location or an address if elsewhere") + where = models.CharField(verbose_name='Where will the event be held?', max_length=500, help_text="Either a specific room at this location or an address if elsewhere") creator = models.ForeignKey(User, related_name="events_created") organizers = models.ManyToManyField(User, related_name="events_organized", blank=True) organizer_notes = models.TextField(blank=True, null=True, help_text="These will only be visible to other organizers") limit = models.IntegerField(default=0, help_text="Specify a cap on the number of RSVPs, or 0 for no limit.", blank=True) - visibility = models.CharField(choices = event_visibility, max_length=200, default=PUBLIC, help_text="Community events are visible only to community members. Private events are visible to those who have the link.") - status = models.CharField(choices = event_statuses, default=PENDING, max_length=200, verbose_name='Review Status', blank=True) + visibility = models.CharField(choices=event_visibility, max_length=200, default=PUBLIC, help_text="Community events are visible only to community members. Private events are visible to those who have the link.") # noqa + status = models.CharField(choices=event_statuses, default=PENDING, max_length=200, verbose_name='Review Status', blank=True) endorsements = models.ManyToManyField(User, related_name="events_endorsed", blank=True) # the location field is optional but lets you associate an event with a # specific location object that is also managed by this software. a single @@ -149,37 +155,38 @@ def is_viewable(self, current_user): is_community_member = False # ok now let's see... - if ( - (self.status == 'live' and self.visibility == Event.PUBLIC) - or - ( - is_event_admin - or - current_user == self.creator - or - current_user in self.organizers.all() - or - current_user in self.attendees.all() - ) - or - (is_community_member and self.visibility != Event.PRIVATE) - ): + can_view = any([ + self.status == 'live' and self.visibility == Event.PUBLIC, + is_event_admin, + current_user == self.creator, + current_user in self.organizers.all(), + current_user in self.attendees.all(), + is_community_member and self.visibility != Event.PRIVATE + + ]) + + if can_view: viewable = True else: viewable = False return viewable + def default_event_status(sender, instance, created, using, **kwargs): logger.debug(instance) logger.debug(created) logger.debug(instance.status) - if created == True: + + if created: if instance.creator in instance.admin.users.all(): instance.status = Event.FEEDBACK else: instance.status = Event.PENDING + + post_save.connect(default_event_status, sender=Event) + class EventNotifications(models.Model): user = models.OneToOneField(User, related_name='event_notifications') # send reminders on day-of the event? @@ -191,8 +198,10 @@ class EventNotifications(models.Model): class Meta: app_label = 'gather' + User.event_notifications = property(lambda u: EventNotifications.objects.get_or_create(user=u)[0]) + # override the save method of the User model to create the EventNotifications # object automatically for new users def add_user_event_notifications(sender, instance, created, using, **kwargs): @@ -200,4 +209,3 @@ def add_user_event_notifications(sender, instance, created, using, **kwargs): # defined with get_or_create, above. instance.event_notifications return - diff --git a/manage.py b/manage.py index 019020c7..a2c5617f 100755 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "modernomad.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "modernomad.settings.local") from django.core.management import execute_from_command_line diff --git a/modernomad/local_settings.example.py b/modernomad/local_settings.example.py deleted file mode 100644 index 4badf22d..00000000 --- a/modernomad/local_settings.example.py +++ /dev/null @@ -1,138 +0,0 @@ -# copy this file to local_settings.py. it should be exluded from the repo -# (ensure local_settings.py is in .gitignore) - -# Make this unique, and don't share it with anybody. -SECRET_KEY = 'secret' - -ADMINS = ( - ('Your Name', 'your@email.com'), - ) - -ALLOWED_HOSTS = ['domain.com'] - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'USER': 'postgres', - 'NAME': 'modernomadb', - 'PASSWORD': 'somepassword', - } -} - -MEDIA_ROOT = "../media" -BACKUP_ROOT = "../backups/" -BACKUP_COUNT = 30 - -# use XS_SHARING_ALLOWED_ORIGINS = '*' for all domains -XS_SHARING_ALLOWED_ORIGINS = "http://localhost:8989/" -XS_SHARING_ALLOWED_METHODS = ['POST', 'GET', 'PUT', 'OPTIONS', 'DELETE'] -XS_SHARING_ALLOWED_HEADERS = ["Content-Type"] - -# what mode are we running in? use this to trigger different settings. -DEVELOPMENT = 0 -PRODUCTION = 1 - -# default mode is dev. change to production as appropriate. -MODE = DEVELOPMENT - -# how many days should people be allowed to make a booking request for? -MAX_BOOKING_DAYS = 14 - -# how many days ahead to send the welcome email to guests with relevan house -# info. -WELCOME_EMAIL_DAYS_AHEAD = 2 - -# this should be a TEST or PRODUCTION key depending on whether this is a local -# test/dev site or production! -STRIPE_SECRET_KEY = "sk_XXXXX" -STRIPE_PUBLISHABLE_KEY = "pk_XXXXX" - -# Discourse discussion group -DISCOURSE_BASE_URL = 'http://your-discourse-site.com' -DISCOURSE_SSO_SECRET = 'paste_your_secret_here' - -MAILGUN_API_KEY = "key-XXXX" - -LIST_DOMAIN = "somedomain.com" - -if MODE == DEVELOPMENT: - EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' - DEBUG = True -else: - EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' - EMAIL_USE_TLS = True - EMAIL_HOST = 'somehost' - EMAIL_PORT = 587 - EMAIL_HOST_USER = 'some@email.com' - EMAIL_HOST_PASSWORD = 'password' - DEBUG = False - -TEMPLATE_DEBUG = DEBUG - -# fill in any local template directories. any templates with the same name WILL -# OVERRIDE included templates. don't forget the trailing slash in the path, and -# a comma at the end of the tuple item if there is only one path. -LOCAL_TEMPLATE_DIRS = ( - # eg, "../local_templates/", - ) - -# celery configuration options -BROKER_URL = 'amqp://' -CELERY_RESULT_BACKEND = 'amqp://' - -CELERY_TASK_SERIALIZER = 'json' -CELERY_RESULT_SERIALIZER = 'json' -CELERY_ENABLE_UTC = True - -# Logging -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", - 'datefmt': "%d/%b/%Y %H:%M:%S" - }, - 'simple': { - 'format': '%(levelname)s %(message)s' - }, - }, - 'handlers': { - 'file': { - 'level': 'DEBUG', - 'class': 'logging.FileHandler', - 'filename': '/Users/jessykate/code/embassynetwork/logs/django.log', - 'formatter': 'verbose', - }, - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler', - 'include_html': True, - 'formatter': 'verbose', - } - }, - 'loggers': { - 'django': { - 'handlers': ['file'], - 'level': 'INFO', - 'propagate': True, - }, - 'django.request': { - 'handlers': ['file', 'mail_admins'], - 'level': 'INFO', - 'propagate': True, - }, - 'core': { - 'handlers': ['file'], - 'level': 'DEBUG', - }, - 'modernomad': { - 'handlers': ['file'], - 'level': 'DEBUG', - }, - 'gather': { - 'handlers': ['file'], - 'level': 'DEBUG', - }, - }, -} diff --git a/modernomad/local_settings.travis.py b/modernomad/local_settings.travis.py deleted file mode 100644 index 891936cf..00000000 --- a/modernomad/local_settings.travis.py +++ /dev/null @@ -1,140 +0,0 @@ -import os - -# copy this file to local_settings.py. it should be exluded from the repo -# (ensure local_settings.py is in .gitignore) - -# Make this unique, and don't share it with anybody. -SECRET_KEY = 'secret' - -ADMINS = ( - ('Craig Ambrose', 'craig@enspiral.com') -) - -ALLOWED_HOSTS = ['domain.com'] - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'USER': 'postgres', - 'NAME': 'modernomadb' - } -} - -MEDIA_ROOT = "../media" -BACKUP_ROOT = "../backups/" -BACKUP_COUNT = 30 - -# use XS_SHARING_ALLOWED_ORIGINS = '*' for all domains -XS_SHARING_ALLOWED_ORIGINS = "http://localhost:8989/" -XS_SHARING_ALLOWED_METHODS = ['POST', 'GET', 'PUT', 'OPTIONS', 'DELETE'] -XS_SHARING_ALLOWED_HEADERS = ["Content-Type"] - -# what mode are we running in? use this to trigger different settings. -DEVELOPMENT = 0 -PRODUCTION = 1 - -# default mode is dev. change to production as appropriate. -MODE = DEVELOPMENT - -# how many days should people be allowed to make a booking request for? -MAX_BOOKING_DAYS = 14 - -# how many days ahead to send the welcome email to guests with relevan house -# info. -WELCOME_EMAIL_DAYS_AHEAD = 2 - -# this should be a TEST or PRODUCTION key depending on whether this is a local -# test/dev site or production! -STRIPE_SECRET_KEY = "sk_XXXXX" -STRIPE_PUBLISHABLE_KEY = "pk_XXXXX" - -# Discourse discussion group -DISCOURSE_BASE_URL = 'http://your-discourse-site.com' -DISCOURSE_SSO_SECRET = 'paste_your_secret_here' - -MAILGUN_API_KEY = "key-XXXX" - -LIST_DOMAIN = "somedomain.com" - -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' -DEBUG = True - -TEMPLATE_DEBUG = DEBUG - -# fill in any local template directories. any templates with the same name WILL -# OVERRIDE included templates. don't forget the trailing slash in the path, and -# a comma at the end of the tuple item if there is only one path. -LOCAL_TEMPLATE_DIRS = ( - # eg, "../local_templates/", - ) - -# celery configuration options -BROKER_URL = 'amqp://' -CELERY_RESULT_BACKEND = 'amqp://' - -CELERY_TASK_SERIALIZER = 'json' -CELERY_RESULT_SERIALIZER = 'json' -CELERY_ENABLE_UTC = True - -ROOT = os.path.dirname(os.path.abspath(__file__)) -BASE_DIR = os.path.normpath(ROOT + '/..') - -WEBPACK_LOADER = { - 'DEFAULT': { - 'BUNDLE_DIR_NAME': '', - 'STATS_FILE': os.path.join(BASE_DIR, 'client/webpack-stats-prod.json'), - } -} - -# Logging -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", - 'datefmt': "%d/%b/%Y %H:%M:%S" - }, - 'simple': { - 'format': '%(levelname)s %(message)s' - }, - }, - 'handlers': { - 'file': { - 'level': 'DEBUG', - 'class': 'logging.FileHandler', - 'filename': './django.log', - 'formatter': 'verbose', - }, - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler', - 'include_html': True, - 'formatter': 'verbose', - } - }, - 'loggers': { - 'django': { - 'handlers': ['file'], - 'level': 'INFO', - 'propagate': True, - }, - 'django.request': { - 'handlers': ['file', 'mail_admins'], - 'level': 'INFO', - 'propagate': True, - }, - 'core': { - 'handlers': ['file'], - 'level': 'DEBUG', - }, - 'modernomad': { - 'handlers': ['file'], - 'level': 'DEBUG', - }, - 'gather': { - 'handlers': ['file'], - 'level': 'DEBUG', - }, - }, -} diff --git a/modernomad/settings/__init__.py b/modernomad/settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modernomad/settings.py b/modernomad/settings/common.py similarity index 62% rename from modernomad/settings.py rename to modernomad/settings/common.py index ab7d081d..068418d7 100644 --- a/modernomad/settings.py +++ b/modernomad/settings/common.py @@ -2,33 +2,30 @@ import os import datetime import sys +from pathlib import Path +import environ -# Make filepaths relative to settings. -ROOT = os.path.dirname(os.path.abspath(__file__)) -BASE_DIR = os.path.normpath(ROOT + '/..') +env = environ.Env() -path = lambda *a: os.path.join(ROOT, *a) +BASE_DIR = Path.cwd() +BACKUP_ROOT = BASE_DIR / 'backups' -BACKUP_ROOT = ROOT + '/backups/' +ADMINS = (( + env('ADMIN_NAME', default='Unnamed'), + env('ADMIN_EMAIL', default='none@example.com') +),) -ADMINS = ( - ('Jessy Kate Schingler', 'jessy@embassynetwork.com'), -) +MANAGERS = ADMINS +ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) -DEBUG = os.getenv('DEBUG', False) -STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY', default='') +DEBUG = False +TEMPLATE_DEBUG = False -MANAGERS = ADMINS +STRIPE_PUBLISHABLE_KEY = env('STRIPE_PUBLISHABLE_KEY', default='') +STRIPE_SECRET_KEY = env('STRIPE_SECRET_KEY', default='') DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': 'modernomad.db', # Or path to database file if using sqlite3. - 'USER': '', # Not used with sqlite3. - 'PASSWORD': '', # Not used with sqlite3. - 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '', # Set to empty string for default. Not used with sqlite3. - } + 'default': env.db('DATABASE_URL', default='sqlite:///modernomad.db'), } # Local time zone for this installation. Choices can be found here: @@ -54,19 +51,27 @@ # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/home/media/media.lawrence.com/media/" -MEDIA_ROOT = path("../media/") +MEDIA_ROOT = BASE_DIR / 'media' # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash. # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" MEDIA_URL = "/media/" +AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME', default='') +AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID', default='') +AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY', default='') +AWS_DEFAULT_ACL = 'public-read' +if AWS_STORAGE_BUCKET_NAME: + DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' + MEDIA_URL = env('MEDIA_URL', default='https://%s.s3.amazonaws.com/' % AWS_STORAGE_BUCKET_NAME) + # Absolute path to the directory static files should be collected to. # Don't put anything in this directory yourself; store your static files # in apps' "static/" subdirectories and in STATICFILES_DIRS. # Example: "/home/media/media.lawrence.com/static/" -STATIC_ROOT = path("../../static/") -STATICFILES_DIRS = ('static', 'client/dist') +STATIC_ROOT = BASE_DIR / 'static' +STATICFILES_DIRS = ('client/dist',) STATIC_URL = '/static/' STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', @@ -81,13 +86,15 @@ ) EMAIL_BACKEND = 'modernomad.backends.MailgunBackend' +MAILGUN_API_KEY = env('MAILGUN_API_KEY', default='') -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - # 'django.template.loaders.eggs.Loader', -) +# this will be used as the subject line prefix for all emails sent from this app. +EMAIL_SUBJECT_PREFIX = env('EMAIL_SUBJECT_PREFIX', default='[Modernomad] ') +DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL', default='stay@example.com') +LIST_DOMAIN = env('LIST_DOMAIN', default='somedomain.com') + +GOOGLE_ANALYTICS_PROPERTY_ID = env('GOOGLE_ANALYTICS_PROPERTY_ID', default='') +GOOGLE_ANALYTICS_DOMAIN = env('GOOGLE_ANALYTICS_DOMAIN', default='example.com') MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', @@ -95,12 +102,18 @@ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - 'modernomad.middleware.crossdomainxhr.CORSMiddleware', 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', + 'modernomad.middleware.crossdomainxhr.CORSMiddleware', # Uncomment the next line for simple clickjacking protection: # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + # 'django.template.loaders.eggs.Loader', +) # default template context processors TEMPLATE_CONTEXT_PROCESSORS = ( "django.contrib.auth.context_processors.auth", @@ -116,6 +129,14 @@ "core.context_processors.analytics.google_analytics", ) +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + BASE_DIR / 'templates', + BASE_DIR / 'core' / 'templates' +) + # other JWT options available at https://github.com/jpadilla/django-jwt-auth JWT_EXPIRATION_DELTA = datetime.timedelta(days=1000) @@ -124,34 +145,15 @@ # Python dotted path to the WSGI application used by Django's runserver. WSGI_APPLICATION = 'modernomad.wsgi.application' -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. - path("../templates/"), - path("core/templates/"), -) - WEBPACK_LOADER = { 'DEFAULT': { 'BUNDLE_DIR_NAME': 'client/build/', - 'STATS_FILE': os.path.join(BASE_DIR, 'client/webpack-stats.json'), + 'STATS_FILE': BASE_DIR / 'client' / 'webpack-stats.json', } } -INSTALLED_APPS = ( - 'core', - 'bank', - 'djcelery', - 'gather', - 'modernomad', - 'api', - 'django_graphiql', - 'graphene_django', - 'graphapi', - 'django_behave', - 'bdd', - 'rest_framework', +INSTALLED_APPS = [ + # django stuff 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -162,12 +164,27 @@ 'django.contrib.flatpages', 'django.contrib.admindocs', 'django.contrib.humanize', - 'webpack_loader', + + # 3rd party 'compressor', + 'django_behave', 'django_extensions', 'django_filters', - # 'debug_toolbar', -) + 'django_graphiql', + 'djcelery', + 'graphene_django', + 'rest_framework', + 'webpack_loader', + + # modernomad + 'core', + 'bank', + 'gather', + 'modernomad', + 'api', + 'bdd', + 'graphapi', +] COMPRESS_PRECOMPILERS = ( ('text/less', 'lessc {infile} {outfile}'), @@ -179,10 +196,6 @@ AUTH_PROFILE_MODULE = 'core.UserProfile' ACCOUNT_ACTIVATION_DAYS = 7 # One week account activation window. -# Discourse discussion group -DISCOURSE_BASE_URL = 'http://your-discourse-site.com' -DISCOURSE_SSO_SECRET = 'paste_your_secret_here' - # If we add a page for the currently-logged-in user to view and edit # their profile, we might want to use that here instead. LOGIN_REDIRECT_URL = '/' @@ -190,32 +203,84 @@ LOGOUT_URL = '/people/logout/' # Celery configuration options -BROKER_URL = "amqp://guest:guest@localhost:5672//" +BROKER_URL = env( + 'BROKER_URL', + default=env('CLOUDAMQP_URL', default='amqp://guest:guest@localhost:5672//') +) CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_ENABLE_UTC = True CELERY_ACCEPT_CONTENT = ['json', 'yaml'] +# Disabled when moving to Heroku for simplicity's sake, because no tasks +# have results. If results are needed, a suitable one can be picked for +# Heroku + CloudAMPQ. (Probably the "rpc" one, perhaps Django ORM?) +CELERY_RESULT_BACKEND = None + +# as per https://www.cloudamqp.com/docs/celery.html +BROKER_POOL_LIMIT = 1 +BROKER_HEARTBEAT = None +BROKER_CONNECTION_TIMEOUT = 30 +CELERY_EVENT_QUEUE_EXPIRES = 60 +CELERYD_PREFETCH_MULTIPLIER = 1 + REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ] } -LIST_DOMAIN = "example.com" - -# import any local settings -try: - from .local_settings import * -except ImportError: - pass - - NOSE_ARGS = [ '--nocapture', '--nologcapture' ] +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", + 'datefmt': "%d/%b/%Y %H:%M:%S" + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'handlers': { + 'file': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': './django.log', + 'formatter': 'simple', + } + }, + 'loggers': { + 'django': { + 'handlers': ['file'], + 'level': 'INFO', + 'propagate': True, + }, + 'django.request': { + 'handlers': ['file'], + 'level': 'INFO', + 'propagate': True, + }, + 'core': { + 'handlers': ['file'], + 'level': 'DEBUG', + }, + 'modernomad': { + 'handlers': ['file'], + 'level': 'DEBUG', + }, + 'gather': { + 'handlers': ['file'], + 'level': 'DEBUG', + }, + }, +} + class DisableMigrations(object): def __contains__(self, item): @@ -234,3 +299,6 @@ def __getitem__(self, item): MIGRATION_MODULES = DisableMigrations() os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = "localhost:8000-8010,8080,9200-9300" + +# Setting unique=True on a ForeignKey has the same effect as using a OneToOneField. +SILENCED_SYSTEM_CHECKS = ["fields.W342"] diff --git a/modernomad/settings/local.py b/modernomad/settings/local.py new file mode 100644 index 00000000..a29a7f40 --- /dev/null +++ b/modernomad/settings/local.py @@ -0,0 +1,11 @@ +from .common import * # noqa +from .common import INSTALLED_APPS + +DEBUG = True +TEMPLATE_DEBUG = True +INSTALLED_APPS = INSTALLED_APPS + [ + 'debug_toolbar' +] +SECRET_KEY = 'local_development' + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/modernomad/settings/production.py b/modernomad/settings/production.py new file mode 100644 index 00000000..028d107e --- /dev/null +++ b/modernomad/settings/production.py @@ -0,0 +1,30 @@ +from .common import * # noqa +from .common import env +from .common import BASE_DIR + +SECRET_KEY = env('SECRET_KEY') + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'level': env('DJANGO_LOG_LEVEL', default='INFO'), + }, + }, +} + +WEBPACK_LOADER = { + 'DEFAULT': { + 'BUNDLE_DIR_NAME': '', + 'CACHE': True, + 'STATS_FILE': BASE_DIR / 'client/webpack-stats-prod.json', + } +} +COMPRESS_OFFLINE = True diff --git a/modernomad/settings/staging.py b/modernomad/settings/staging.py new file mode 100644 index 00000000..67424445 --- /dev/null +++ b/modernomad/settings/staging.py @@ -0,0 +1,4 @@ +from .common import * # noqa +from .common import env + +SECRET_KEY = env('SECRET_KEY') diff --git a/modernomad/settings_docker.py b/modernomad/settings_docker.py deleted file mode 100644 index 2007e6fa..00000000 --- a/modernomad/settings_docker.py +++ /dev/null @@ -1,98 +0,0 @@ -# A sensible set of defaults for a production environment which can be -# overridden with environment variables - -import environ -from django.core.exceptions import ImproperlyConfigured -import os -from .settings import * - -env = environ.Env() - -# what mode are we running in? use this to trigger different settings. -DEVELOPMENT = 0 -PRODUCTION = 1 - -# default mode is production. change to dev as appropriate. -env_mode = env('MODE', default='PRODUCTION') -if env_mode == 'DEVELOPMENT': - MODE = DEVELOPMENT - DEBUG = True - EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' -elif env_mode == 'PRODUCTION': - MODE = PRODUCTION - DEBUG = False - - STATIC_ROOT = path("static_root/") - - # Collect static gathers client/dist/ into root of static/, - # which is why bundle dir is blank - WEBPACK_LOADER = { - 'DEFAULT': { - 'BUNDLE_DIR_NAME': '', - 'CACHE': True, - 'STATS_FILE': os.path.join(BASE_DIR, 'client/webpack-stats-prod.json'), - } - } - COMPRESS_OFFLINE = True -else: - raise ImproperlyConfigured('Unknown MODE setting') - -TEMPLATE_DEBUG = DEBUG - -SECRET_KEY = env('SECRET_KEY') -DATABASES = { - 'default': env.db('DATABASE_URL', default='postgres://postgres@postgres/postgres'), -} - -ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) - -BROKER_URL = env('BROKER_URL', default=env('CLOUDAMQP_URL', default='amqp://guest:guest@rabbitmq//')) -CELERY_RESULT_BACKEND = BROKER_URL - -# this should be a TEST or PRODUCTION key depending on whether this is a local -# test/dev site or production! -STRIPE_SECRET_KEY = env('STRIPE_SECRET_KEY', default='') -STRIPE_PUBLISHABLE_KEY = env('STRIPE_PUBLISHABLE_KEY', default='') - -# Discourse discussion group -DISCOURSE_BASE_URL = env('DISCOURSE_BASE_URL', default='') -DISCOURSE_SSO_SECRET = env('DISCOURSE_SSO_SECRET', default='') - -ADMINS = (( - env('ADMIN_NAME', default='Unnamed'), - env('ADMIN_EMAIL', default='none@example.com') -),) - -MAILGUN_API_KEY = env('MAILGUN_API_KEY', default='') -LIST_DOMAIN = env('LIST_DOMAIN', default='somedomain.com') -EMAIL_SUBJECT_PREFIX = env('EMAIL_SUBJECT_PREFIX', default='[Modernomad] ') -DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL', default='stay@example.com') - -GOOGLE_ANALYTICS_PROPERTY_ID = env('GOOGLE_ANALYTICS_PROPERTY_ID', default='') -GOOGLE_ANALYTICS_DOMAIN = env('GOOGLE_ANALYTICS_DOMAIN', default='example.com') - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - }, - }, - 'loggers': { - 'django': { - 'handlers': ['console'], - 'level': env('DJANGO_LOG_LEVEL', default='INFO'), - }, - }, -} - -# Media storage -AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME', default='') -AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID', default='') -AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY', default='') -AWS_DEFAULT_ACL = 'public-read' -if AWS_STORAGE_BUCKET_NAME: - DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' - MEDIA_URL = env('MEDIA_URL', default='https://%s.s3.amazonaws.com/' % AWS_STORAGE_BUCKET_NAME) - diff --git a/modernomad/urls/main.py b/modernomad/urls/main.py index 011f05ee..e4605782 100644 --- a/modernomad/urls/main.py +++ b/modernomad/urls/main.py @@ -1,7 +1,7 @@ from django.conf.urls import patterns, include, url from django.contrib import admin from modernomad.urls import user -from modernomad import settings +from django.conf import settings from django.views.generic import RedirectView from django.http import HttpResponse, HttpResponseRedirect from rest_framework import routers, serializers, viewsets diff --git a/modernomad/wsgi.py b/modernomad/wsgi.py index 5b76deea..60bda68b 100644 --- a/modernomad/wsgi.py +++ b/modernomad/wsgi.py @@ -15,7 +15,7 @@ """ import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "modernomad.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "modernomad.settings.production") # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION diff --git a/requirements.txt b/requirements.txt index c155c5ac..b173ca41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ celery==3.1.20 django-behave django-celery==3.1.17 django-compressor==2.2 -django-debug-toolbar==1.4 +django-debug-toolbar==1.5 django-environ==0.4.3 django-extensions==2.1.3 django-filter==0.15.3 diff --git a/templates/index.html b/templates/index.html index 9a08db57..308ef4f3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,7 +7,7 @@

- Embassy Network + ʞɹoʍʇǝN ʎssɐqɯƎ

A network of place-based communities experimenting with new forms of governance and solidarity.