forked from data.coop/membersystem
Compare commits
No commits in common. "35d8438d6608b408e660952e04856c788457aba4" and "1add5f3e35defa92d512a18cbcde4b2ad066d710" have entirely different histories.
35d8438d66
...
1add5f3e35
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,5 +2,4 @@ __pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.sw*
|
*.sw*
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
project/settings/local.py
|
membersystem/settings/local.py
|
||||||
.pytest_cache
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
repos:
|
|
||||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v1.3.0
|
|
||||||
hooks:
|
|
||||||
- id: trailing-whitespace
|
|
||||||
- id: flake8
|
|
||||||
- id: check-yaml
|
|
||||||
- id: check-added-large-files
|
|
||||||
- id: debug-statements
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- repo: https://github.com/asottile/reorder_python_imports
|
|
||||||
rev: v1.0.1
|
|
||||||
hooks:
|
|
||||||
- id: reorder-python-imports
|
|
15
Makefile
15
Makefile
|
@ -1,15 +0,0 @@
|
||||||
# These are just some make targets, expressing how things
|
|
||||||
# are supposed to be run, but feel free to change them!
|
|
||||||
|
|
||||||
dev-setup:
|
|
||||||
pip install -r requirements_dev.txt --upgrade
|
|
||||||
pip install -r requirements.txt --upgrade
|
|
||||||
pre-commit install
|
|
||||||
python manage.py migrate
|
|
||||||
python manage.py createsuperuser
|
|
||||||
|
|
||||||
lint:
|
|
||||||
pre-commit run --all
|
|
||||||
|
|
||||||
test:
|
|
||||||
pytest
|
|
19
README.rst
19
README.rst
|
@ -1,19 +0,0 @@
|
||||||
member.data.coop
|
|
||||||
================
|
|
||||||
|
|
||||||
To start developing:
|
|
||||||
|
|
||||||
# Create a virtualenv with python 3
|
|
||||||
$ mkvirtualenv -p python3 datacoop
|
|
||||||
|
|
||||||
# Run this make target, which installs all the requirements and sets up a
|
|
||||||
# development database.
|
|
||||||
$ make dev-setup
|
|
||||||
|
|
||||||
To run the Django development server:
|
|
||||||
|
|
||||||
$ python manage.py runserver
|
|
||||||
|
|
||||||
Before you push your stuff, run tests:
|
|
||||||
|
|
||||||
$ make test
|
|
|
@ -1,28 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from . import models
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Order)
|
|
||||||
class OrderAdmin(admin.ModelAdmin):
|
|
||||||
|
|
||||||
list_display = ('who', 'description', 'created', 'is_paid',)
|
|
||||||
|
|
||||||
def who(self, instance):
|
|
||||||
return instance.user.get_full_name()
|
|
||||||
who.short_description = _("Customer")
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Payment)
|
|
||||||
class PaymentAdmin(admin.ModelAdmin):
|
|
||||||
|
|
||||||
list_display = ('who', 'description', 'order_id', 'created',)
|
|
||||||
|
|
||||||
def who(self, instance):
|
|
||||||
return instance.order.user.get_full_name()
|
|
||||||
who.short_description = _("Customer")
|
|
||||||
|
|
||||||
def order_id(self, instance):
|
|
||||||
return instance.order.id
|
|
||||||
order_id.short_description = _("Order ID")
|
|
|
@ -1,5 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class AccountingConfig(AppConfig):
|
|
||||||
name = 'accounting'
|
|
|
@ -1,84 +0,0 @@
|
||||||
# Generated by Django 2.0.6 on 2018-06-23 19:51
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import djmoney.models.fields
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Account',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
|
||||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Order',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
|
||||||
('description', models.CharField(max_length=1024, verbose_name='description')),
|
|
||||||
('price_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
|
||||||
('price', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16, verbose_name='price (excl. VAT)')),
|
|
||||||
('vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
|
||||||
('vat', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16, verbose_name='VAT')),
|
|
||||||
('is_paid', models.BooleanField(default=False, verbose_name='is paid')),
|
|
||||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='accounting.Account')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Order',
|
|
||||||
'verbose_name_plural': 'Orders',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Payment',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
|
||||||
('amount_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
|
||||||
('amount', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16)),
|
|
||||||
('description', models.CharField(max_length=1024, verbose_name='description')),
|
|
||||||
('stripe_charge_id', models.CharField(blank=True, max_length=255, null=True)),
|
|
||||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='accounting.Order')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'payment',
|
|
||||||
'verbose_name_plural': 'payments',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Transaction',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
|
||||||
('amount_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
|
||||||
('amount', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), help_text='This will include VAT', max_digits=16, verbose_name='amount')),
|
|
||||||
('description', models.CharField(max_length=1024, verbose_name='description')),
|
|
||||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions', to='accounting.Account')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,157 +0,0 @@
|
||||||
from hashlib import md5
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db import models
|
|
||||||
from django.db.models.aggregates import Sum
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.utils.translation import pgettext_lazy
|
|
||||||
from djmoney.models.fields import MoneyField
|
|
||||||
|
|
||||||
|
|
||||||
class CreatedModifiedAbstract(models.Model):
|
|
||||||
|
|
||||||
modified = models.DateTimeField(
|
|
||||||
auto_now=True,
|
|
||||||
verbose_name=_("modified"),
|
|
||||||
)
|
|
||||||
created = models.DateTimeField(
|
|
||||||
auto_now_add=True,
|
|
||||||
verbose_name=_("created"),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class Account(CreatedModifiedAbstract):
|
|
||||||
"""
|
|
||||||
This is the model where we can give access to several users, such that they
|
|
||||||
can decide which account to use to pay for something.
|
|
||||||
"""
|
|
||||||
|
|
||||||
owner = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def balance(self):
|
|
||||||
return self.transactions.all().aggregate(Sum('amount')).get('amount', 0)
|
|
||||||
|
|
||||||
|
|
||||||
class Transaction(CreatedModifiedAbstract):
|
|
||||||
"""
|
|
||||||
Tracks in and outgoing events of an account. When an order is received, an
|
|
||||||
amount is subtracted, when a payment is received, an amount is added.
|
|
||||||
"""
|
|
||||||
|
|
||||||
account = models.ForeignKey(
|
|
||||||
Account,
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name="transactions",
|
|
||||||
)
|
|
||||||
amount = MoneyField(
|
|
||||||
verbose_name=_("amount"),
|
|
||||||
max_digits=16,
|
|
||||||
decimal_places=2,
|
|
||||||
help_text=_("This will include VAT")
|
|
||||||
)
|
|
||||||
description = models.CharField(
|
|
||||||
max_length=1024,
|
|
||||||
verbose_name=_("description")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Order(CreatedModifiedAbstract):
|
|
||||||
"""
|
|
||||||
Scoped out: Contents of invoices will have to be tracked either here or in
|
|
||||||
a separate Invoice model. This is undecided because we are not generating
|
|
||||||
invoices at the moment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
|
|
||||||
account = models.ForeignKey(Account, on_delete=models.PROTECT)
|
|
||||||
is_paid = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
description = models.CharField(
|
|
||||||
max_length=1024,
|
|
||||||
verbose_name=_("description")
|
|
||||||
)
|
|
||||||
|
|
||||||
price = MoneyField(
|
|
||||||
verbose_name=_("price (excl. VAT)"),
|
|
||||||
max_digits=16,
|
|
||||||
decimal_places=2,
|
|
||||||
)
|
|
||||||
vat = MoneyField(
|
|
||||||
verbose_name=_("VAT"),
|
|
||||||
max_digits=16,
|
|
||||||
decimal_places=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
is_paid = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
verbose_name=_("is paid"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total(self):
|
|
||||||
return self.price + self.vat
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_id(self):
|
|
||||||
return str(self.id).zfill(6)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def payment_token(self):
|
|
||||||
pk = str(self.pk).encode("utf-8")
|
|
||||||
x = md5()
|
|
||||||
x.update(pk)
|
|
||||||
extra_hash = (settings.SECRET_KEY + 'blah').encode('utf-8')
|
|
||||||
x.update(extra_hash)
|
|
||||||
return x.hexdigest()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = pgettext_lazy("accounting term", "Order")
|
|
||||||
verbose_name_plural = pgettext_lazy("accounting term", "Orders")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Order ID {id}".format(id=self.display_id)
|
|
||||||
|
|
||||||
|
|
||||||
class Payment(CreatedModifiedAbstract):
|
|
||||||
|
|
||||||
amount = MoneyField(
|
|
||||||
max_digits=16,
|
|
||||||
decimal_places=2,
|
|
||||||
)
|
|
||||||
order = models.ForeignKey(Order, on_delete=models.PROTECT)
|
|
||||||
|
|
||||||
description = models.CharField(
|
|
||||||
max_length=1024,
|
|
||||||
verbose_name=_("description")
|
|
||||||
)
|
|
||||||
|
|
||||||
stripe_charge_id = models.CharField(
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_id(self):
|
|
||||||
return str(self.id).zfill(6)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_order(cls, order):
|
|
||||||
return cls.objects.create(
|
|
||||||
order=order,
|
|
||||||
user=order.user,
|
|
||||||
amount=order.total,
|
|
||||||
description=order.description,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Payment ID {id}".format(id=self.display_id)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("payment")
|
|
||||||
verbose_name_plural = _("payments")
|
|
|
@ -1,16 +0,0 @@
|
||||||
import pytest
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
|
||||||
from . import models
|
|
||||||
|
|
||||||
# @pytest.fixture
|
|
||||||
# def test():
|
|
||||||
# do stuff
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_balance():
|
|
||||||
user = User.objects.create_user("test", "lala@adas.com", "1234")
|
|
||||||
account = models.Account.objects.create(
|
|
||||||
owner=user
|
|
||||||
)
|
|
||||||
assert account.balance == 0
|
|
|
@ -3,7 +3,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "membersystem.settings")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
@ -11,5 +11,5 @@ if __name__ == "__main__":
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
"available on your PYTHONPATH environment variable? Did you "
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
"forget to activate a virtual environment?"
|
"forget to activate a virtual environment?"
|
||||||
)
|
) from exc
|
||||||
execute_from_command_line(sys.argv)
|
execute_from_command_line(sys.argv)
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
"""
|
|
||||||
Membership application
|
|
||||||
======================
|
|
||||||
|
|
||||||
This application's domain relate to organizational structures and
|
|
||||||
implementation of statutes, policies etc.
|
|
||||||
|
|
||||||
"""
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from . import models
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Membership)
|
|
||||||
class MembershipAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
|
@ -1,5 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class MembershipConfig(AppConfig):
|
|
||||||
name = 'membership'
|
|
|
@ -1,101 +0,0 @@
|
||||||
# Generated by Django 2.0.6 on 2018-06-23 19:07
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import djmoney.models.fields
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Membership',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
|
||||||
('can_vote', models.BooleanField(default=False, help_text='Indicates that the user has a democratic membership of the organization.', verbose_name='can vote')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'membership',
|
|
||||||
'verbose_name_plural': 'memberships',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Organization',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
|
||||||
('name', models.CharField(max_length=64, verbose_name='name')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'organization',
|
|
||||||
'verbose_name_plural': 'organizations',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Subscription',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
|
||||||
('active', models.BooleanField(default=False, help_text='Automatically set by payment system.', verbose_name='active')),
|
|
||||||
('starts', models.DateField()),
|
|
||||||
('ends', models.DateField()),
|
|
||||||
('renewed_subscription', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='membership.Subscription', verbose_name='renewed subscription')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'subscription',
|
|
||||||
'verbose_name_plural': 'subscriptions',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SubscriptionType',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
|
||||||
('name', models.CharField(max_length=64, verbose_name='name')),
|
|
||||||
('fee_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
|
||||||
('fee', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16)),
|
|
||||||
('fee_vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
|
||||||
('fee_vat', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0'), max_digits=16)),
|
|
||||||
('duration', models.PositiveSmallIntegerField(choices=[(1, 'annual')], default=1, verbose_name='duration')),
|
|
||||||
('organization', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.Organization')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'subscription type',
|
|
||||||
'verbose_name_plural': 'subscription types',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='subscription',
|
|
||||||
name='subscription_type',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='membership.SubscriptionType', verbose_name='subscription type'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='subscription',
|
|
||||||
name='user',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='membership',
|
|
||||||
name='organization',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.Organization'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='membership',
|
|
||||||
name='user',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,144 +0,0 @@
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db import models
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from djmoney.models.fields import MoneyField
|
|
||||||
|
|
||||||
|
|
||||||
class CreatedModifiedAbstract(models.Model):
|
|
||||||
|
|
||||||
modified = models.DateTimeField(
|
|
||||||
auto_now=True,
|
|
||||||
verbose_name=_("modified"),
|
|
||||||
)
|
|
||||||
created = models.DateTimeField(
|
|
||||||
auto_now_add=True,
|
|
||||||
verbose_name=_("created"),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class Organization(CreatedModifiedAbstract):
|
|
||||||
"""
|
|
||||||
This holds the data of the organization that someone is a member of. It is
|
|
||||||
possible that we'll create more advanced features here.
|
|
||||||
"""
|
|
||||||
name = models.CharField(
|
|
||||||
verbose_name=_("name"),
|
|
||||||
max_length=64,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("organization")
|
|
||||||
verbose_name_plural = _("organizations")
|
|
||||||
|
|
||||||
|
|
||||||
class Membership(CreatedModifiedAbstract):
|
|
||||||
"""
|
|
||||||
A user remains a member of an organization even though the subscription is
|
|
||||||
unpaid or renewed. This just changes the status/permissions etc. of the
|
|
||||||
membership, thus we need to track subscription creation, expiry, renewals
|
|
||||||
etc. and ensure that the membership is modified accordingly.
|
|
||||||
|
|
||||||
This expresses some
|
|
||||||
"""
|
|
||||||
|
|
||||||
organization = models.ForeignKey(Organization, on_delete=models.PROTECT)
|
|
||||||
user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
|
|
||||||
|
|
||||||
can_vote = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
verbose_name=_("can vote"),
|
|
||||||
help_text=_(
|
|
||||||
"Indicates that the user has a democratic membership of the "
|
|
||||||
"organization."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
|
|
||||||
return _("{} is a member of {}").format(
|
|
||||||
self.user.get_full_name(),
|
|
||||||
self.organization.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("membership")
|
|
||||||
verbose_name_plural = _("memberships")
|
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionType(CreatedModifiedAbstract):
|
|
||||||
"""
|
|
||||||
Properties of subscriptions are stored here. Should of course not be edited
|
|
||||||
after subscriptions are created.
|
|
||||||
"""
|
|
||||||
|
|
||||||
organization = models.ForeignKey(Organization, on_delete=models.PROTECT)
|
|
||||||
|
|
||||||
name = models.CharField(
|
|
||||||
verbose_name=_("name"),
|
|
||||||
max_length=64,
|
|
||||||
)
|
|
||||||
|
|
||||||
fee = MoneyField(
|
|
||||||
max_digits=16,
|
|
||||||
decimal_places=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
fee_vat = MoneyField(
|
|
||||||
max_digits=16,
|
|
||||||
decimal_places=2,
|
|
||||||
default=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
duration = models.PositiveSmallIntegerField(
|
|
||||||
default=1,
|
|
||||||
choices=[(1, _("annual"))],
|
|
||||||
verbose_name=_("duration"),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("subscription type")
|
|
||||||
verbose_name_plural = _("subscription types")
|
|
||||||
|
|
||||||
|
|
||||||
class Subscription(CreatedModifiedAbstract):
|
|
||||||
"""
|
|
||||||
To not confuse other types of subscriptions, one can be a *subscribed*
|
|
||||||
member of an organization, meaning that they are paying etc.
|
|
||||||
|
|
||||||
A subscription does not track payment, this is done in the accounting app.
|
|
||||||
"""
|
|
||||||
|
|
||||||
subscription_type = models.ForeignKey(
|
|
||||||
SubscriptionType,
|
|
||||||
related_name='memberships',
|
|
||||||
verbose_name=_("subscription type"),
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
)
|
|
||||||
user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
|
|
||||||
|
|
||||||
active = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
verbose_name=_("active"),
|
|
||||||
help_text=_("Automatically set by payment system.")
|
|
||||||
)
|
|
||||||
|
|
||||||
starts = models.DateField()
|
|
||||||
ends = models.DateField()
|
|
||||||
|
|
||||||
renewed_subscription = models.ForeignKey(
|
|
||||||
'self',
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("renewed subscription"),
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("subscription")
|
|
||||||
verbose_name_plural = _("subscriptions")
|
|
10
membersystem/settings/__init__.py
Normal file
10
membersystem/settings/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from .base import *
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .local import *
|
||||||
|
except ImportError:
|
||||||
|
warnings.warn("No settings.local, using a default SECRET_KEY 'hest'")
|
||||||
|
SECRET_KEY = "hest"
|
||||||
|
pass
|
|
@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/2.0/topics/settings/
|
||||||
For the full list of settings and their values, see
|
For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/2.0/ref/settings/
|
https://docs.djangoproject.com/en/2.0/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
@ -19,7 +20,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = False
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
@ -35,9 +36,10 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django.contrib.sites',
|
'django.contrib.sites',
|
||||||
|
|
||||||
'users',
|
'profiles',
|
||||||
'accounting',
|
|
||||||
'membership',
|
'allauth',
|
||||||
|
'allauth.account',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -50,12 +52,12 @@ MIDDLEWARE = [
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'project.urls'
|
ROOT_URLCONF = 'membersystem.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [os.path.join(BASE_DIR, "project", "templates")],
|
'DIRS': [],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
|
@ -63,7 +65,6 @@ TEMPLATES = [
|
||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
'project.context_processors.current_site',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -74,7 +75,7 @@ AUTHENTICATION_BACKENDS = (
|
||||||
'allauth.account.auth_backends.AuthenticationBackend',
|
'allauth.account.auth_backends.AuthenticationBackend',
|
||||||
)
|
)
|
||||||
|
|
||||||
WSGI_APPLICATION = 'project.wsgi.application'
|
WSGI_APPLICATION = 'membersystem.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
|
@ -93,16 +94,16 @@ DATABASES = {
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -129,6 +130,3 @@ STATIC_URL = '/static/'
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
|
||||||
CURRENCIES = ('DKK',)
|
|
||||||
CURRENCY_CHOICES = [('DKK', 'DKK')]
|
|
|
@ -1,7 +1,10 @@
|
||||||
"""URLs for the membersystem"""
|
"""URLs for the membersystem"""
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.conf.urls import url
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
url(r'^accounts/', include('allauth.urls')),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
]
|
]
|
|
@ -6,10 +6,11 @@ It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
For more information on this file, see
|
For more information on this file, see
|
||||||
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
|
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "membersystem.settings")
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
5
profiles/apps.py
Normal file
5
profiles/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ProfilesConfig(AppConfig):
|
||||||
|
name = 'profiles'
|
3
profiles/tests.py
Normal file
3
profiles/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
|
@ -1,9 +0,0 @@
|
||||||
"""Context processors for the membersystem app."""
|
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
|
||||||
|
|
||||||
|
|
||||||
def current_site(request):
|
|
||||||
"""Include the current site in the context."""
|
|
||||||
return {
|
|
||||||
'site': get_current_site(request)
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
from .base import * # noqa
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .local import * # noqa
|
|
||||||
except ImportError:
|
|
||||||
warnings.warn(
|
|
||||||
"No settings.local, using a default SECRET_KEY 'hest'. You should "
|
|
||||||
"write a custom local.py with this setting."
|
|
||||||
)
|
|
||||||
SECRET_KEY = "hest"
|
|
||||||
DEBUG = True
|
|
||||||
pass
|
|
|
@ -1,79 +0,0 @@
|
||||||
/* General styles */
|
|
||||||
html
|
|
||||||
{
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 2.5vmin;
|
|
||||||
background: #f8f8f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
body
|
|
||||||
{
|
|
||||||
background: #fff;
|
|
||||||
color: #000;
|
|
||||||
margin: 1em auto;
|
|
||||||
max-width: 50em;
|
|
||||||
padding: 0 1em;
|
|
||||||
box-shadow: 0 0 2.5em rgba(0, 0, 0, 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
header,
|
|
||||||
footer
|
|
||||||
{
|
|
||||||
background: #eee;
|
|
||||||
padding: .5em;
|
|
||||||
margin: 0 -1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer
|
|
||||||
{
|
|
||||||
margin-top: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
header h1
|
|
||||||
{
|
|
||||||
font-size: 1em;
|
|
||||||
float: left;
|
|
||||||
padding: .5em .5em;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul,
|
|
||||||
footer ul
|
|
||||||
{
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul li,
|
|
||||||
footer ul li
|
|
||||||
{
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
header ul li a,
|
|
||||||
footer ul li a
|
|
||||||
{
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
padding: .5em .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Forms */
|
|
||||||
label
|
|
||||||
{
|
|
||||||
display: block;
|
|
||||||
padding: .5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
|
||||||
input,
|
|
||||||
textarea
|
|
||||||
{
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
{% load static %}
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>{% block head_title %}{% endblock %} – {{ site.name }}</title>
|
|
||||||
{% block extra_head %}{% endblock %}
|
|
||||||
<link rel="stylesheet" href="{% static '/css/membersystem.css' %}" type="text/css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>
|
|
||||||
<a href="/">{{ site.name }}</a>
|
|
||||||
</h1>
|
|
||||||
<ul>
|
|
||||||
{% if user.is_authenticated %}
|
|
||||||
<li><a href="{% url 'account_email' %}">Change e-mail</a></li>
|
|
||||||
<li><a href="{% url 'account_logout' %}">Sign out</a></li>
|
|
||||||
{% else %}
|
|
||||||
<li><a href="{% url 'account_login' %}">Sign in</a></li>
|
|
||||||
<li><a href="{% url 'account_signup' %}">Sign up</a></li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</header>
|
|
||||||
{% block body %}
|
|
||||||
{% if messages %}
|
|
||||||
<ul id="messages">
|
|
||||||
{% for message in messages %}
|
|
||||||
<li>{{message}}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% endblock %}
|
|
||||||
{% endblock %}
|
|
||||||
{% block extra_body %}
|
|
||||||
{% endblock %}
|
|
||||||
<footer>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://data.coop">data.coop</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://git.data.coop/data.coop/membersystem">source code</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,5 +0,0 @@
|
||||||
[pytest]
|
|
||||||
testpaths = .
|
|
||||||
python_files = tests.py test_*.py *_tests.py
|
|
||||||
DJANGO_SETTINGS_MODULE = project.settings
|
|
||||||
#norecursedirs = dist tmp* .svn .*
|
|
|
@ -1,3 +1,2 @@
|
||||||
Django==2.0.6
|
Django==2.0.6
|
||||||
django-money==0.14
|
django-allauth==0.36.0
|
||||||
django-extensions==2.0.7
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
pytest
|
|
||||||
pytest-django
|
|
||||||
pre-commit
|
|
13
setup.cfg
13
setup.cfg
|
@ -1,13 +0,0 @@
|
||||||
[flake8]
|
|
||||||
ignore = E226,E302,E41
|
|
||||||
max-line-length = 160
|
|
||||||
max-complexity = 10
|
|
||||||
exclude = */migrations/*
|
|
||||||
|
|
||||||
[isort]
|
|
||||||
atomic = true
|
|
||||||
multi_line_output = 5
|
|
||||||
line_length = 160
|
|
||||||
indent = ' '
|
|
||||||
combine_as_imports = true
|
|
||||||
skip = wsgi.py,.eggs,setup.py
|
|
|
@ -1,5 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class UsersConfig(AppConfig):
|
|
||||||
name = 'users'
|
|
|
@ -1,24 +0,0 @@
|
||||||
# Generated by Django 2.0.6 on 2018-06-23 19:45
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Profile',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
Loading…
Reference in a new issue