From 998879cc633d561473386673352e5684a12a9951 Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Tue, 11 Apr 2017 22:26:31 +0200 Subject: [PATCH 001/343] Add basic models and expose them via admin. --- payshare/purchases/admin.py | 8 +++ payshare/purchases/migrations/0001_initial.py | 64 ++++++++++++++++++ payshare/purchases/models.py | 66 ++++++++++++++++++- payshare/settings.py | 2 + requirements.in | 1 + requirements.txt | 9 +++ 6 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 payshare/purchases/migrations/0001_initial.py diff --git a/payshare/purchases/admin.py b/payshare/purchases/admin.py index 13be29d..3b15bc6 100644 --- a/payshare/purchases/admin.py +++ b/payshare/purchases/admin.py @@ -3,4 +3,12 @@ from django.contrib import admin +from payshare.purchases.models import Collective +from payshare.purchases.models import Membership +from payshare.purchases.models import Purchase + # Register your models here. + +admin.site.register(Collective) +admin.site.register(Membership) +admin.site.register(Purchase) diff --git a/payshare/purchases/migrations/0001_initial.py b/payshare/purchases/migrations/0001_initial.py new file mode 100644 index 0000000..eef5a24 --- /dev/null +++ b/payshare/purchases/migrations/0001_initial.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-11 20:25 +from __future__ import unicode_literals + +from decimal import Decimal +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import djmoney.models.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Collective', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=100)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Membership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('collective', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='purchases.Collective')), + ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Purchase', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=100)), + ('description', models.TextField(blank=True, null=True)), + ('price_currency', djmoney.models.fields.CurrencyField(choices=[('AFN', 'Afghani'), ('DZD', 'Algerian Dinar'), ('ARS', 'Argentine Peso'), ('AMD', 'Armenian Dram'), ('AWG', 'Aruban Guilder'), ('AUD', 'Australian Dollar'), ('AZN', 'Azerbaijanian Manat'), ('BSD', 'Bahamian Dollar'), ('BHD', 'Bahraini Dinar'), ('THB', 'Baht'), ('BBD', 'Barbados Dollar'), ('BYR', 'Belarussian Ruble'), ('BZD', 'Belize Dollar'), ('BMD', 'Bermudian Dollar (customarily known as Bermuda Dollar)'), ('BTN', 'Bhutanese ngultrum'), ('VEF', 'Bolivar Fuerte'), ('XBA', 'Bond Markets Units European Composite Unit (EURCO)'), ('BRL', 'Brazilian Real'), ('BND', 'Brunei Dollar'), ('BGN', 'Bulgarian Lev'), ('BIF', 'Burundi Franc'), ('XOF', 'CFA Franc BCEAO'), ('XAF', 'CFA franc BEAC'), ('XPF', 'CFP Franc'), ('CAD', 'Canadian Dollar'), ('CVE', 'Cape Verde Escudo'), ('KYD', 'Cayman Islands Dollar'), ('CLP', 'Chilean peso'), ('XTS', 'Codes specifically reserved for testing purposes'), ('COP', 'Colombian peso'), ('KMF', 'Comoro Franc'), ('CDF', 'Congolese franc'), ('BAM', 'Convertible Marks'), ('NIO', 'Cordoba Oro'), ('CRC', 'Costa Rican Colon'), ('HRK', 'Croatian Kuna'), ('CUP', 'Cuban Peso'), ('CUC', 'Cuban convertible peso'), ('CZK', 'Czech Koruna'), ('GMD', 'Dalasi'), ('DKK', 'Danish Krone'), ('MKD', 'Denar'), ('DJF', 'Djibouti Franc'), ('STD', 'Dobra'), ('DOP', 'Dominican Peso'), ('VND', 'Dong'), ('XCD', 'East Caribbean Dollar'), ('EGP', 'Egyptian Pound'), ('ETB', 'Ethiopian Birr'), ('EUR', 'Euro'), ('XBB', 'European Monetary Unit (E.M.U.-6)'), ('XBD', 'European Unit of Account 17(E.U.A.-17)'), ('XBC', 'European Unit of Account 9(E.U.A.-9)'), ('FKP', 'Falkland Islands Pound'), ('FJD', 'Fiji Dollar'), ('HUF', 'Forint'), ('GHS', 'Ghana Cedi'), ('GIP', 'Gibraltar Pound'), ('XAU', 'Gold'), ('XFO', 'Gold-Franc'), ('PYG', 'Guarani'), ('GNF', 'Guinea Franc'), ('GYD', 'Guyana Dollar'), ('HTG', 'Haitian gourde'), ('HKD', 'Hong Kong Dollar'), ('UAH', 'Hryvnia'), ('ISK', 'Iceland Krona'), ('INR', 'Indian Rupee'), ('IRR', 'Iranian Rial'), ('IQD', 'Iraqi Dinar'), ('IMP', 'Isle of Man pount'), ('JMD', 'Jamaican Dollar'), ('JOD', 'Jordanian Dinar'), ('KES', 'Kenyan Shilling'), ('PGK', 'Kina'), ('LAK', 'Kip'), ('KWD', 'Kuwaiti Dinar'), ('AOA', 'Kwanza'), ('MMK', 'Kyat'), ('GEL', 'Lari'), ('LVL', 'Latvian Lats'), ('LBP', 'Lebanese Pound'), ('ALL', 'Lek'), ('HNL', 'Lempira'), ('SLL', 'Leone'), ('LSL', 'Lesotho loti'), ('LRD', 'Liberian Dollar'), ('LYD', 'Libyan Dinar'), ('SZL', 'Lilangeni'), ('LTL', 'Lithuanian Litas'), ('MGA', 'Malagasy Ariary'), ('MWK', 'Malawian Kwacha'), ('MYR', 'Malaysian Ringgit'), ('TMM', 'Manat'), ('MUR', 'Mauritius Rupee'), ('MZN', 'Metical'), ('MXN', 'Mexican peso'), ('MDL', 'Moldovan Leu'), ('MAD', 'Moroccan Dirham'), ('NGN', 'Naira'), ('ERN', 'Nakfa'), ('NAD', 'Namibian Dollar'), ('NPR', 'Nepalese Rupee'), ('ANG', 'Netherlands Antillian Guilder'), ('ILS', 'New Israeli Sheqel'), ('RON', 'New Leu'), ('TWD', 'New Taiwan Dollar'), ('NZD', 'New Zealand Dollar'), ('KPW', 'North Korean Won'), ('NOK', 'Norwegian Krone'), ('PEN', 'Nuevo Sol'), ('MRO', 'Ouguiya'), ('TOP', 'Paanga'), ('PKR', 'Pakistan Rupee'), ('XPD', 'Palladium'), ('MOP', 'Pataca'), ('PHP', 'Philippine Peso'), ('XPT', 'Platinum'), ('GBP', 'Pound Sterling'), ('BWP', 'Pula'), ('QAR', 'Qatari Rial'), ('GTQ', 'Quetzal'), ('ZAR', 'Rand'), ('OMR', 'Rial Omani'), ('KHR', 'Riel'), ('MVR', 'Rufiyaa'), ('IDR', 'Rupiah'), ('RUB', 'Russian Ruble'), ('RWF', 'Rwanda Franc'), ('XDR', 'SDR'), ('SHP', 'Saint Helena Pound'), ('SAR', 'Saudi Riyal'), ('RSD', 'Serbian Dinar'), ('SCR', 'Seychelles Rupee'), ('XAG', 'Silver'), ('SGD', 'Singapore Dollar'), ('SBD', 'Solomon Islands Dollar'), ('KGS', 'Som'), ('SOS', 'Somali Shilling'), ('TJS', 'Somoni'), ('LKR', 'Sri Lanka Rupee'), ('SDG', 'Sudanese Pound'), ('SRD', 'Surinam Dollar'), ('SEK', 'Swedish Krona'), ('CHF', 'Swiss Franc'), ('SYP', 'Syrian Pound'), ('BDT', 'Taka'), ('WST', 'Tala'), ('TZS', 'Tanzanian Shilling'), ('KZT', 'Tenge'), ('TTD', 'Trinidad and Tobago Dollar'), ('MNT', 'Tugrik'), ('TND', 'Tunisian Dinar'), ('TRY', 'Turkish Lira'), ('TVD', 'Tuvalu dollar'), ('AED', 'UAE Dirham'), ('XFU', 'UIC-Franc'), ('USD', 'US Dollar'), ('UGX', 'Uganda Shilling'), ('UYU', 'Uruguayan peso'), ('UZS', 'Uzbekistan Sum'), ('VUV', 'Vatu'), ('KRW', 'Won'), ('YER', 'Yemeni Rial'), ('JPY', 'Yen'), ('CNY', 'Yuan Renminbi'), ('ZMK', 'Zambian Kwacha'), ('ZMW', 'Zambian Kwacha'), ('ZWD', 'Zimbabwe Dollar A/06'), ('ZWN', 'Zimbabwe dollar A/08'), ('ZWL', 'Zimbabwe dollar A/09'), ('PLN', 'Zloty')], default='EUR', editable=False, max_length=3)), + ('price', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), default_currency=b'EUR', max_digits=10)), + ('buyer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('collective', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='purchases.Collective')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/payshare/purchases/models.py b/payshare/purchases/models.py index 1dfab76..4ccd02f 100644 --- a/payshare/purchases/models.py +++ b/payshare/purchases/models.py @@ -2,5 +2,69 @@ from __future__ import unicode_literals from django.db import models +from django.db.models.signals import pre_save +from django.dispatch import receiver +from django.utils import timezone -# Create your models here. +from djmoney.models.fields import MoneyField + + +class PayShareError(Exception): + pass + + +class BuyerNotMemberOfCollectiveError(PayShareError): + pass + + +class TimestampMixin(models.Model): + created_at = models.DateTimeField(default=timezone.now) + modified_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + +class Collective(TimestampMixin, models.Model): + name = models.CharField(max_length=100) + + def is_member(self, user): + try: + Membership.objects.get(collective=self, member=user) + return True + except Membership.DoesNotExist: + return False + + def __unicode__(self): + return u"{}".format(self.name) + + +class Membership(TimestampMixin, models.Model): + member = models.ForeignKey("auth.User") + collective = models.ForeignKey("purchases.Collective") + + def __unicode__(self): + return u"{} in {}".format(self.member.username, + self.collective.name) + + +class Purchase(TimestampMixin, models.Model): + name = models.CharField(max_length=100) + description = models.TextField(blank=True, null=True) + price = MoneyField(max_digits=10, + decimal_places=2, + default_currency='EUR') + buyer = models.ForeignKey("auth.User") + collective = models.ForeignKey("purchases.Collective") + + def __unicode__(self): + return u"{} for {} by {} in {}".format(self.price, + self.name, + self.buyer.username, + self.collective.name) + + +@receiver(pre_save, sender=Purchase) +def purchase_pre_save_ensure_membership(sender, instance, *args, **kwargs): + if not instance.collective.is_member(instance.buyer): + raise BuyerNotMemberOfCollectiveError() diff --git a/payshare/settings.py b/payshare/settings.py index 25f6f36..1c64530 100644 --- a/payshare/settings.py +++ b/payshare/settings.py @@ -37,6 +37,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + + 'payshare.purchases', ] MIDDLEWARE = [ diff --git a/requirements.in b/requirements.in index d3e4ba5..66b740a 100644 --- a/requirements.in +++ b/requirements.in @@ -1 +1,2 @@ django +django-money diff --git a/requirements.txt b/requirements.txt index 7f3847b..25e7133 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,14 @@ # # pip-compile --output-file requirements.txt requirements.in # +appdirs==1.4.3 # via setuptools +django-money==0.10.2 django==1.11 +packaging==16.8 # via setuptools +py-moneyed==0.6.0 # via django-money +pyparsing==2.2.0 # via packaging pytz==2017.2 # via django +six==1.10.0 # via packaging, setuptools + +# The following packages are considered to be unsafe in a requirements file: +# setuptools # via django-money From a8de49c164bb1a703c803b7d0f94f34748503a21 Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Tue, 11 Apr 2017 23:31:23 +0200 Subject: [PATCH 002/343] Add docstrings. --- payshare/purchases/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/payshare/purchases/models.py b/payshare/purchases/models.py index 4ccd02f..662c58d 100644 --- a/payshare/purchases/models.py +++ b/payshare/purchases/models.py @@ -18,6 +18,7 @@ class BuyerNotMemberOfCollectiveError(PayShareError): class TimestampMixin(models.Model): + """Add created and modified timestamps to a model.""" created_at = models.DateTimeField(default=timezone.now) modified_at = models.DateTimeField(auto_now=True) @@ -26,6 +27,7 @@ class Meta: class Collective(TimestampMixin, models.Model): + """A collective groups users that want to share payments.""" name = models.CharField(max_length=100) def is_member(self, user): @@ -40,6 +42,7 @@ def __unicode__(self): class Membership(TimestampMixin, models.Model): + """A membership is a mapping of a user to a collective.""" member = models.ForeignKey("auth.User") collective = models.ForeignKey("purchases.Collective") @@ -49,6 +52,7 @@ def __unicode__(self): class Purchase(TimestampMixin, models.Model): + """A purchase describes a certain payment of a member of a collective.""" name = models.CharField(max_length=100) description = models.TextField(blank=True, null=True) price = MoneyField(max_digits=10, From 3eb9882dd5c070e31664180b11d0793e434de09a Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Tue, 11 Apr 2017 23:31:47 +0200 Subject: [PATCH 003/343] Add unfinished form and template. --- payshare/purchases/forms.py | 11 +++++ payshare/purchases/templates/index.html | 46 +++++++++++++++++++ payshare/purchases/templatetags/__init__.py | 0 payshare/purchases/templatetags/customtags.py | 9 ++++ payshare/purchases/views.py | 25 +++++++++- payshare/urls.py | 3 ++ 6 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 payshare/purchases/forms.py create mode 100644 payshare/purchases/templates/index.html create mode 100644 payshare/purchases/templatetags/__init__.py create mode 100644 payshare/purchases/templatetags/customtags.py diff --git a/payshare/purchases/forms.py b/payshare/purchases/forms.py new file mode 100644 index 0000000..77007c7 --- /dev/null +++ b/payshare/purchases/forms.py @@ -0,0 +1,11 @@ +from django import forms + +from payshare.purchases.models import Collective +from payshare.purchases.models import Membership +from payshare.purchases.models import Purchase + + +class PurchaseForm(forms.ModelForm): + class Meta: + model = Purchase + fields = ["name", "description", "price", "buyer", "collective"] diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html new file mode 100644 index 0000000..b55127d --- /dev/null +++ b/payshare/purchases/templates/index.html @@ -0,0 +1,46 @@ + + + + + Payshare + + + + + + + + + {% load customtags %} +
+
+ {% csrf_token %} +
+ {{ purchase_form.name.errors }} + {{ purchase_form.name.label_tag }} + {{ purchase_form.name }} +
+
+ {{ purchase_form.price.errors }} + {{ purchase_form.price.label_tag }} + {{ purchase_form.price }} +
+
+ {{ purchase_form.buyer.errors }} + {{ purchase_form.buyer.label_tag }} + {{ purchase_form.buyer }} +
+
+ {{ purchase_form.collective.errors }} + {{ purchase_form.collective.label_tag }} + {{ purchase_form.collective }} +
+ +
+
+ + diff --git a/payshare/purchases/templatetags/__init__.py b/payshare/purchases/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payshare/purchases/templatetags/customtags.py b/payshare/purchases/templatetags/customtags.py new file mode 100644 index 0000000..933a3b4 --- /dev/null +++ b/payshare/purchases/templatetags/customtags.py @@ -0,0 +1,9 @@ +from django import template + +register = template.Library() + + +@register.filter(name="addclass") +def addclass(value, arg): + """Use this tag to add a CSS class e.g. to a form element.""" + return value.as_widget(attrs={"class": arg}) diff --git a/payshare/purchases/views.py b/payshare/purchases/views.py index e784a0b..6afc315 100644 --- a/payshare/purchases/views.py +++ b/payshare/purchases/views.py @@ -2,5 +2,28 @@ from __future__ import unicode_literals from django.shortcuts import render +# from django.http import HttpResponseRedirect -# Create your views here. +from payshare.purchases.models import Collective +from payshare.purchases.forms import PurchaseForm + + +def index(request): + # # if this is a POST request we need to process the form data + # if request.method == "POST": + # # create a form instance and populate it with data from the request: + # form = PurchaseForm(request.POST) + # # check whether it"s valid: + # if form.is_valid(): + # # process the data in form.cleaned_data as required + # # ... + # # redirect to a new URL: + # return HttpResponseRedirect("/thanks/") + # # if a GET (or any other method) we"ll create a blank form + # else: + # form = PurchaseForm() + collective = Collective.objects.first() + purchase_form = PurchaseForm(initial={"collective": collective}) + return render(request, "index.html", { + "purchase_form": purchase_form, + }) diff --git a/payshare/urls.py b/payshare/urls.py index 1d94658..be0b958 100644 --- a/payshare/urls.py +++ b/payshare/urls.py @@ -16,6 +16,9 @@ from django.conf.urls import url from django.contrib import admin +from payshare.purchases import views as purchases_views + urlpatterns = [ url(r'^admin/', admin.site.urls), + url(r'^', purchases_views.index), ] From 343a0814a8d479a7e942843252c3827a4d9666e5 Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Sat, 22 Apr 2017 13:15:24 +0200 Subject: [PATCH 004/343] Add the Liquidation model. --- payshare/purchases/admin.py | 3 +- .../migrations/0002_add_liquidation_model.py | 38 ++++++++++++++++ payshare/purchases/models.py | 43 +++++++++++++++++-- 3 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 payshare/purchases/migrations/0002_add_liquidation_model.py diff --git a/payshare/purchases/admin.py b/payshare/purchases/admin.py index 3b15bc6..6c3ed93 100644 --- a/payshare/purchases/admin.py +++ b/payshare/purchases/admin.py @@ -6,9 +6,10 @@ from payshare.purchases.models import Collective from payshare.purchases.models import Membership from payshare.purchases.models import Purchase +from payshare.purchases.models import Liquidation -# Register your models here. admin.site.register(Collective) admin.site.register(Membership) admin.site.register(Purchase) +admin.site.register(Liquidation) diff --git a/payshare/purchases/migrations/0002_add_liquidation_model.py b/payshare/purchases/migrations/0002_add_liquidation_model.py new file mode 100644 index 0000000..ac3413d --- /dev/null +++ b/payshare/purchases/migrations/0002_add_liquidation_model.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-22 11:04 +from __future__ import unicode_literals + +from decimal import Decimal +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('purchases', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Liquidation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('description', models.TextField(blank=True, null=True)), + ('amount_currency', djmoney.models.fields.CurrencyField(choices=[('AFN', 'Afghani'), ('DZD', 'Algerian Dinar'), ('ARS', 'Argentine Peso'), ('AMD', 'Armenian Dram'), ('AWG', 'Aruban Guilder'), ('AUD', 'Australian Dollar'), ('AZN', 'Azerbaijanian Manat'), ('BSD', 'Bahamian Dollar'), ('BHD', 'Bahraini Dinar'), ('THB', 'Baht'), ('BBD', 'Barbados Dollar'), ('BYR', 'Belarussian Ruble'), ('BZD', 'Belize Dollar'), ('BMD', 'Bermudian Dollar (customarily known as Bermuda Dollar)'), ('BTN', 'Bhutanese ngultrum'), ('VEF', 'Bolivar Fuerte'), ('XBA', 'Bond Markets Units European Composite Unit (EURCO)'), ('BRL', 'Brazilian Real'), ('BND', 'Brunei Dollar'), ('BGN', 'Bulgarian Lev'), ('BIF', 'Burundi Franc'), ('XOF', 'CFA Franc BCEAO'), ('XAF', 'CFA franc BEAC'), ('XPF', 'CFP Franc'), ('CAD', 'Canadian Dollar'), ('CVE', 'Cape Verde Escudo'), ('KYD', 'Cayman Islands Dollar'), ('CLP', 'Chilean peso'), ('XTS', 'Codes specifically reserved for testing purposes'), ('COP', 'Colombian peso'), ('KMF', 'Comoro Franc'), ('CDF', 'Congolese franc'), ('BAM', 'Convertible Marks'), ('NIO', 'Cordoba Oro'), ('CRC', 'Costa Rican Colon'), ('HRK', 'Croatian Kuna'), ('CUP', 'Cuban Peso'), ('CUC', 'Cuban convertible peso'), ('CZK', 'Czech Koruna'), ('GMD', 'Dalasi'), ('DKK', 'Danish Krone'), ('MKD', 'Denar'), ('DJF', 'Djibouti Franc'), ('STD', 'Dobra'), ('DOP', 'Dominican Peso'), ('VND', 'Dong'), ('XCD', 'East Caribbean Dollar'), ('EGP', 'Egyptian Pound'), ('ETB', 'Ethiopian Birr'), ('EUR', 'Euro'), ('XBB', 'European Monetary Unit (E.M.U.-6)'), ('XBD', 'European Unit of Account 17(E.U.A.-17)'), ('XBC', 'European Unit of Account 9(E.U.A.-9)'), ('FKP', 'Falkland Islands Pound'), ('FJD', 'Fiji Dollar'), ('HUF', 'Forint'), ('GHS', 'Ghana Cedi'), ('GIP', 'Gibraltar Pound'), ('XAU', 'Gold'), ('XFO', 'Gold-Franc'), ('PYG', 'Guarani'), ('GNF', 'Guinea Franc'), ('GYD', 'Guyana Dollar'), ('HTG', 'Haitian gourde'), ('HKD', 'Hong Kong Dollar'), ('UAH', 'Hryvnia'), ('ISK', 'Iceland Krona'), ('INR', 'Indian Rupee'), ('IRR', 'Iranian Rial'), ('IQD', 'Iraqi Dinar'), ('IMP', 'Isle of Man pount'), ('JMD', 'Jamaican Dollar'), ('JOD', 'Jordanian Dinar'), ('KES', 'Kenyan Shilling'), ('PGK', 'Kina'), ('LAK', 'Kip'), ('KWD', 'Kuwaiti Dinar'), ('AOA', 'Kwanza'), ('MMK', 'Kyat'), ('GEL', 'Lari'), ('LVL', 'Latvian Lats'), ('LBP', 'Lebanese Pound'), ('ALL', 'Lek'), ('HNL', 'Lempira'), ('SLL', 'Leone'), ('LSL', 'Lesotho loti'), ('LRD', 'Liberian Dollar'), ('LYD', 'Libyan Dinar'), ('SZL', 'Lilangeni'), ('LTL', 'Lithuanian Litas'), ('MGA', 'Malagasy Ariary'), ('MWK', 'Malawian Kwacha'), ('MYR', 'Malaysian Ringgit'), ('TMM', 'Manat'), ('MUR', 'Mauritius Rupee'), ('MZN', 'Metical'), ('MXN', 'Mexican peso'), ('MDL', 'Moldovan Leu'), ('MAD', 'Moroccan Dirham'), ('NGN', 'Naira'), ('ERN', 'Nakfa'), ('NAD', 'Namibian Dollar'), ('NPR', 'Nepalese Rupee'), ('ANG', 'Netherlands Antillian Guilder'), ('ILS', 'New Israeli Sheqel'), ('RON', 'New Leu'), ('TWD', 'New Taiwan Dollar'), ('NZD', 'New Zealand Dollar'), ('KPW', 'North Korean Won'), ('NOK', 'Norwegian Krone'), ('PEN', 'Nuevo Sol'), ('MRO', 'Ouguiya'), ('TOP', 'Paanga'), ('PKR', 'Pakistan Rupee'), ('XPD', 'Palladium'), ('MOP', 'Pataca'), ('PHP', 'Philippine Peso'), ('XPT', 'Platinum'), ('GBP', 'Pound Sterling'), ('BWP', 'Pula'), ('QAR', 'Qatari Rial'), ('GTQ', 'Quetzal'), ('ZAR', 'Rand'), ('OMR', 'Rial Omani'), ('KHR', 'Riel'), ('MVR', 'Rufiyaa'), ('IDR', 'Rupiah'), ('RUB', 'Russian Ruble'), ('RWF', 'Rwanda Franc'), ('XDR', 'SDR'), ('SHP', 'Saint Helena Pound'), ('SAR', 'Saudi Riyal'), ('RSD', 'Serbian Dinar'), ('SCR', 'Seychelles Rupee'), ('XAG', 'Silver'), ('SGD', 'Singapore Dollar'), ('SBD', 'Solomon Islands Dollar'), ('KGS', 'Som'), ('SOS', 'Somali Shilling'), ('TJS', 'Somoni'), ('LKR', 'Sri Lanka Rupee'), ('SDG', 'Sudanese Pound'), ('SRD', 'Surinam Dollar'), ('SEK', 'Swedish Krona'), ('CHF', 'Swiss Franc'), ('SYP', 'Syrian Pound'), ('BDT', 'Taka'), ('WST', 'Tala'), ('TZS', 'Tanzanian Shilling'), ('KZT', 'Tenge'), ('TTD', 'Trinidad and Tobago Dollar'), ('MNT', 'Tugrik'), ('TND', 'Tunisian Dinar'), ('TRY', 'Turkish Lira'), ('TVD', 'Tuvalu dollar'), ('AED', 'UAE Dirham'), ('XFU', 'UIC-Franc'), ('USD', 'US Dollar'), ('UGX', 'Uganda Shilling'), ('UYU', 'Uruguayan peso'), ('UZS', 'Uzbekistan Sum'), ('VUV', 'Vatu'), ('KRW', 'Won'), ('YER', 'Yemeni Rial'), ('JPY', 'Yen'), ('CNY', 'Yuan Renminbi'), ('ZMK', 'Zambian Kwacha'), ('ZMW', 'Zambian Kwacha'), ('ZWD', 'Zimbabwe Dollar A/06'), ('ZWN', 'Zimbabwe dollar A/08'), ('ZWL', 'Zimbabwe dollar A/09'), ('PLN', 'Zloty')], default='EUR', editable=False, max_length=3)), + ('amount', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), default_currency=b'EUR', max_digits=10)), + ('collective', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='purchases.Collective')), + ('creditor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='creditor', to=settings.AUTH_USER_MODEL)), + ('debtor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='debtor', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/payshare/purchases/models.py b/payshare/purchases/models.py index 662c58d..983c219 100644 --- a/payshare/purchases/models.py +++ b/payshare/purchases/models.py @@ -13,8 +13,18 @@ class PayShareError(Exception): pass -class BuyerNotMemberOfCollectiveError(PayShareError): - pass +class UserNotMemberOfCollectiveError(PayShareError): + + def __init__(self, user, collective): + message = "{} is not part of collective {}".format(user, collective) + super(UserNotMemberOfCollectiveError, self).__init__(message) + + +class LiquidationNeedsTwoDifferentUsersError(PayShareError): + + def __init__(self, user): + message = "{} cannot be both debtor and creditor".format(user) + super(LiquidationNeedsTwoDifferentUsersError, self).__init__(message) class TimestampMixin(models.Model): @@ -71,4 +81,31 @@ def __unicode__(self): @receiver(pre_save, sender=Purchase) def purchase_pre_save_ensure_membership(sender, instance, *args, **kwargs): if not instance.collective.is_member(instance.buyer): - raise BuyerNotMemberOfCollectiveError() + raise UserNotMemberOfCollectiveError(instance.buyer, + instance.collective) + + +class Liquidation(TimestampMixin, models.Model): + """A liquidation describes a repayment of one member to another.""" + description = models.TextField(blank=True, null=True) + amount = MoneyField(max_digits=10, + decimal_places=2, + default_currency='EUR') + debtor = models.ForeignKey("auth.User", related_name="debtor") + creditor = models.ForeignKey("auth.User", related_name="creditor") + collective = models.ForeignKey("purchases.Collective") + + def __unicode__(self): + return u"{} from {} to {} in {}".format(self.price, + self.debtor.username, + self.creditor.username, + self.collective.name) + + +@receiver(pre_save, sender=Liquidation) +def liquidation_pre_save_ensure_constraints(sender, instance, *args, **kwargs): + if instance.debtor == instance.creditor: + raise LiquidationNeedsTwoDifferentUsersError(instance.debtor) + for user in [instance.debtor, instance.creditor]: + if not instance.collective.is_member(user): + raise UserNotMemberOfCollectiveError(user, instance.collective) From 933998ce7bd4442ada768a428c09e8c674d61fe9 Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Sat, 22 Apr 2017 14:34:58 +0200 Subject: [PATCH 005/343] Add django-bootstrap form dependency. --- payshare/settings.py | 2 ++ requirements.in | 1 + requirements.txt | 1 + 3 files changed, 4 insertions(+) diff --git a/payshare/settings.py b/payshare/settings.py index 1c64530..7421b46 100644 --- a/payshare/settings.py +++ b/payshare/settings.py @@ -38,6 +38,8 @@ 'django.contrib.messages', 'django.contrib.staticfiles', + 'bootstrapform', + 'payshare.purchases', ] diff --git a/requirements.in b/requirements.in index 66b740a..73ea6e2 100644 --- a/requirements.in +++ b/requirements.in @@ -1,2 +1,3 @@ django django-money +django-bootstrap-form diff --git a/requirements.txt b/requirements.txt index 25e7133..86aa928 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ # pip-compile --output-file requirements.txt requirements.in # appdirs==1.4.3 # via setuptools +django-bootstrap-form==3.2.1 django-money==0.10.2 django==1.11 packaging==16.8 # via setuptools From 9d82d0667aa7c2608049c5bbbcfa93718de0083a Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Sat, 22 Apr 2017 14:35:16 +0200 Subject: [PATCH 006/343] Add LiquidationForm and work on the main page layout. --- payshare/purchases/forms.py | 11 +- payshare/purchases/templates/index.html | 154 ++++++++++++++++++++---- payshare/purchases/views.py | 8 +- 3 files changed, 144 insertions(+), 29 deletions(-) diff --git a/payshare/purchases/forms.py b/payshare/purchases/forms.py index 77007c7..f0c73d2 100644 --- a/payshare/purchases/forms.py +++ b/payshare/purchases/forms.py @@ -1,11 +1,16 @@ from django import forms -from payshare.purchases.models import Collective -from payshare.purchases.models import Membership from payshare.purchases.models import Purchase +from payshare.purchases.models import Liquidation class PurchaseForm(forms.ModelForm): class Meta: model = Purchase - fields = ["name", "description", "price", "buyer", "collective"] + fields = ["name", "price", "buyer", "collective"] + + +class LiquidationForm(forms.ModelForm): + class Meta: + model = Liquidation + fields = ["amount", "debtor", "creditor", "collective"] diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html index b55127d..377bc60 100644 --- a/payshare/purchases/templates/index.html +++ b/payshare/purchases/templates/index.html @@ -8,39 +8,147 @@ + + + {% load bootstrap %} {% load customtags %} -
-
- {% csrf_token %} -
- {{ purchase_form.name.errors }} - {{ purchase_form.name.label_tag }} - {{ purchase_form.name }} + +
+ +
+

{{ collective.name }}

+
+
Created at {{ collective.created_at|date }}
+
{{ collective.membership_set.count }} Users
+
{{ collective.purchase_set.count }} Purchases
+
{{ collective.liquidation_set.count }} Liquidations
+
+ + +
-
- {{ purchase_form.price.errors }} - {{ purchase_form.price.label_tag }} - {{ purchase_form.price }} +
+
+ {% for membership in collective.membership_set.all %} +
+ {{ membership.member.username }} +
+

{{ membership.member.username }}

+

+ 93.50 € +

+
+
+ {% endfor %}
-
- {{ purchase_form.buyer.errors }} - {{ purchase_form.buyer.label_tag }} - {{ purchase_form.buyer }} +
+ + diff --git a/payshare/purchases/views.py b/payshare/purchases/views.py index 6afc315..ee1f711 100644 --- a/payshare/purchases/views.py +++ b/payshare/purchases/views.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals from django.shortcuts import render -# from django.http import HttpResponseRedirect from payshare.purchases.models import Collective from payshare.purchases.forms import PurchaseForm +from payshare.purchases.forms import LiquidationForm def index(request): @@ -23,7 +23,9 @@ def index(request): # else: # form = PurchaseForm() collective = Collective.objects.first() - purchase_form = PurchaseForm(initial={"collective": collective}) return render(request, "index.html", { - "purchase_form": purchase_form, + "collective": collective, + "purchase_form": PurchaseForm(initial={"collective": collective}), + "liquidation_form": LiquidationForm( + initial={"collective": collective}), }) From b9e04f112309aa4f919e4c5cf7917d80524d267c Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Sat, 22 Apr 2017 14:40:15 +0200 Subject: [PATCH 007/343] Make stats and toolbar buttons wrap in narrow layout. --- payshare/purchases/templates/index.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html index 377bc60..ddcf494 100644 --- a/payshare/purchases/templates/index.html +++ b/payshare/purchases/templates/index.html @@ -23,6 +23,7 @@ .stats { display: flex; + flex-flow: row wrap; justify-content: space-between; } @@ -46,11 +47,12 @@ .toolbar { display: flex; - justify-content: flex-end; + flex-flow: row wrap; + justify-content: center; } .toolbar .btn { - margin: 0 5px 0 5px; + margin: 5px; } .outcome { From 7e148ee2a2217052e457bd888583086ee26eab2b Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Mon, 24 Apr 2017 20:03:19 +0200 Subject: [PATCH 008/343] List purchases for the current collective. --- payshare/purchases/templates/index.html | 25 +++++++++++++++++++++++++ payshare/purchases/views.py | 1 + 2 files changed, 26 insertions(+) diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html index ddcf494..91a7b51 100644 --- a/payshare/purchases/templates/index.html +++ b/payshare/purchases/templates/index.html @@ -45,6 +45,18 @@ box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); } + .purchase-list { + display: flex; + flex-flow: column wrap; + align-items: center; + } + + .card.purchase { + min-width: 50%; + margin: 5px 10px 5px 10px; + box-shadow: none; + } + .toolbar { display: flex; flex-flow: row wrap; @@ -151,6 +163,19 @@
+
+ {% for purchase in purchases %} +
+
+ {{ purchase.buyer.username }} + {{ purchase.buyer }} paid {{ purchase.price }} for {{ purchase.name }} +
+
+ {% endfor %} +
+
diff --git a/payshare/purchases/views.py b/payshare/purchases/views.py index ee1f711..9245364 100644 --- a/payshare/purchases/views.py +++ b/payshare/purchases/views.py @@ -28,4 +28,5 @@ def index(request): "purchase_form": PurchaseForm(initial={"collective": collective}), "liquidation_form": LiquidationForm( initial={"collective": collective}), + "purchases": collective.purchase_set.all().order_by("created_at"), }) From a381aed0a247575e1fe1fd5d1335132ff48c6717 Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Thu, 27 Apr 2017 19:56:46 +0200 Subject: [PATCH 009/343] Adjust some colors and margins. --- payshare/purchases/templates/index.html | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html index 91a7b51..ac26d87 100644 --- a/payshare/purchases/templates/index.html +++ b/payshare/purchases/templates/index.html @@ -13,6 +13,11 @@ From d354d3e5585fd73ecc96bec2b65661b29d97643b Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Fri, 28 Apr 2017 18:48:15 +0200 Subject: [PATCH 010/343] Disable form for now. --- payshare/purchases/templates/index.html | 23 ++++++++---- payshare/purchases/views.py | 47 +++++++++++++++++-------- payshare/settings.py | 1 + payshare/urls.py | 10 ++++-- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html index ac26d87..5b40090 100644 --- a/payshare/purchases/templates/index.html +++ b/payshare/purchases/templates/index.html @@ -56,6 +56,13 @@ align-items: center; } + .purchase-price { + font-weight: bold; + font-size: 1.5em; + /*opacity: 0.4;*/ + color: #9AC8EF; + } + .card.purchase { min-width: 50%; margin: 3px 10px 3px 10px; @@ -88,8 +95,9 @@ + {% load humanize %} + {% load bootstrap %} - {% load customtags %}
@@ -97,9 +105,9 @@

{{ collective.name }}

Created at {{ collective.created_at|date }}
-
{{ collective.membership_set.count }} Users
-
{{ collective.purchase_set.count }} Purchases
-
{{ collective.liquidation_set.count }} Liquidations
+
{{ collective.membership_set.count|apnumber|capfirst }} Users
+
{{ collective.purchase_set.count|apnumber|capfirst }} Purchases
+
{{ collective.liquidation_set.count|apnumber|capfirst }} Liquidations
-

+
+ {{ overall_purchased }} spent overall +
{% for member in members %}
diff --git a/payshare/purchases/views.py b/payshare/purchases/views.py index fa0e2a7..f8a976f 100644 --- a/payshare/purchases/views.py +++ b/payshare/purchases/views.py @@ -39,6 +39,7 @@ def index(request): "collective": collective, "members": members, "purchases": purchases, + "overall_purchased": overall_purchased, "member_summary": member_summary, "purchase_form": PurchaseForm(initial={"collective": collective}), "liquidation_form": LiquidationForm( From 8b2f72fb627d4a4f6ed55e46ecbc6e4a4d631edd Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Fri, 28 Apr 2017 20:17:38 +0200 Subject: [PATCH 014/343] Add a unique key to every collective. --- payshare/purchases/admin.py | 6 +++++- .../0003_add_uuid_key_to_collective.py | 21 +++++++++++++++++++ payshare/purchases/models.py | 2 ++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 payshare/purchases/migrations/0003_add_uuid_key_to_collective.py diff --git a/payshare/purchases/admin.py b/payshare/purchases/admin.py index 6c3ed93..78c55e9 100644 --- a/payshare/purchases/admin.py +++ b/payshare/purchases/admin.py @@ -9,7 +9,11 @@ from payshare.purchases.models import Liquidation -admin.site.register(Collective) +class CollectiveAdmin(admin.ModelAdmin): + readonly_fields = ["key"] + + +admin.site.register(Collective, CollectiveAdmin) admin.site.register(Membership) admin.site.register(Purchase) admin.site.register(Liquidation) diff --git a/payshare/purchases/migrations/0003_add_uuid_key_to_collective.py b/payshare/purchases/migrations/0003_add_uuid_key_to_collective.py new file mode 100644 index 0000000..495b5e6 --- /dev/null +++ b/payshare/purchases/migrations/0003_add_uuid_key_to_collective.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-28 18:16 +from __future__ import unicode_literals + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('purchases', '0002_add_liquidation_model'), + ] + + operations = [ + migrations.AddField( + model_name='collective', + name='key', + field=models.UUIDField(default=uuid.uuid4, editable=False), + ), + ] diff --git a/payshare/purchases/models.py b/payshare/purchases/models.py index 983c219..4fa8fe5 100644 --- a/payshare/purchases/models.py +++ b/payshare/purchases/models.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import uuid from django.db import models from django.db.models.signals import pre_save @@ -39,6 +40,7 @@ class Meta: class Collective(TimestampMixin, models.Model): """A collective groups users that want to share payments.""" name = models.CharField(max_length=100) + key = models.UUIDField(default=uuid.uuid4, editable=False) def is_member(self, user): try: From 46a73db069e75ef6cbbaf810d688c2508b13249d Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Fri, 28 Apr 2017 22:11:57 +0200 Subject: [PATCH 015/343] Ensure memberships are unique. --- .../0004_make_memberships_unique.py | 21 +++++++++++++++++++ payshare/purchases/models.py | 3 +++ 2 files changed, 24 insertions(+) create mode 100644 payshare/purchases/migrations/0004_make_memberships_unique.py diff --git a/payshare/purchases/migrations/0004_make_memberships_unique.py b/payshare/purchases/migrations/0004_make_memberships_unique.py new file mode 100644 index 0000000..8e86394 --- /dev/null +++ b/payshare/purchases/migrations/0004_make_memberships_unique.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-28 20:06 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('purchases', '0003_add_uuid_key_to_collective'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='membership', + unique_together=set([('member', 'collective')]), + ), + ] diff --git a/payshare/purchases/models.py b/payshare/purchases/models.py index 4fa8fe5..5d1112e 100644 --- a/payshare/purchases/models.py +++ b/payshare/purchases/models.py @@ -58,6 +58,9 @@ class Membership(TimestampMixin, models.Model): member = models.ForeignKey("auth.User") collective = models.ForeignKey("purchases.Collective") + class Meta: + unique_together = ("member", "collective") + def __unicode__(self): return u"{} in {}".format(self.member.username, self.collective.name) From df1e95c3bfc45023ac44fbbaf23ddff2dd3639ca Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Fri, 28 Apr 2017 22:12:11 +0200 Subject: [PATCH 016/343] Wrap user cards if needed. --- payshare/purchases/templates/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html index 8c1b22f..f43f7bd 100644 --- a/payshare/purchases/templates/index.html +++ b/payshare/purchases/templates/index.html @@ -39,6 +39,7 @@ .card-container { display: flex; + flex-flow: row wrap; justify-content: center; } @@ -46,7 +47,7 @@ width: 200px; display: flex; justify-content: space-around; - margin: 0 15px 0 15px; + margin: 1em; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); transition: all .3s ease-in-out; } From c693d112f690476ab5c1c6470739346df0f304fc Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Fri, 28 Apr 2017 22:12:24 +0200 Subject: [PATCH 017/343] Add todos. --- todo.org | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 todo.org diff --git a/todo.org b/todo.org new file mode 100644 index 0000000..ba58018 --- /dev/null +++ b/todo.org @@ -0,0 +1,5 @@ +* TODO [0/2] + + - [ ] consider liquidations when calculating balance + - [ ] implement modal to add purchase + - [ ] implement modal to add liquidation From 795c88b499cc1c51ef43340dbfe9ac4d21ecf4b8 Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Tue, 2 May 2017 19:51:42 +0200 Subject: [PATCH 018/343] Fix string repr. --- payshare/purchases/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/payshare/purchases/models.py b/payshare/purchases/models.py index 5d1112e..0f5ea5a 100644 --- a/payshare/purchases/models.py +++ b/payshare/purchases/models.py @@ -101,9 +101,9 @@ class Liquidation(TimestampMixin, models.Model): collective = models.ForeignKey("purchases.Collective") def __unicode__(self): - return u"{} from {} to {} in {}".format(self.price, - self.debtor.username, + return u"{} from {} to {} in {}".format(self.amount, self.creditor.username, + self.debtor.username, self.collective.name) From 39e4502a2e65f13a7ed0c6386e510f4e79c25033 Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Tue, 2 May 2017 19:51:54 +0200 Subject: [PATCH 019/343] Add todos. --- todo.org | 2 ++ 1 file changed, 2 insertions(+) diff --git a/todo.org b/todo.org index ba58018..03e30fc 100644 --- a/todo.org +++ b/todo.org @@ -3,3 +3,5 @@ - [ ] consider liquidations when calculating balance - [ ] implement modal to add purchase - [ ] implement modal to add liquidation + - [ ] filtering for member, price, time, title, ... + - [ ] sorting by price, member, time, title, ... From 6bd966b3335a0ab85ef92c7bffe22831b6980867 Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Tue, 2 May 2017 19:52:28 +0200 Subject: [PATCH 020/343] Add liquidations into the calculation and display. --- payshare/purchases/templates/index.html | 43 +++++++++++++++++++------ payshare/purchases/views.py | 26 +++++++++++++-- todo.org | 4 +-- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html index f43f7bd..aaf1467 100644 --- a/payshare/purchases/templates/index.html +++ b/payshare/purchases/templates/index.html @@ -64,7 +64,7 @@ text-align: center; } - .purchase-list { + .transaction-list { display: flex; flex-flow: column wrap; align-items: center; @@ -211,24 +211,49 @@
-
- {% for purchase in purchases %} +
+ {% for typ, transaction in transactions %} + {% if typ == 'purchase' %}
- {{ purchase.buyer.username }} - - {{ purchase.buyer }} paid for {{ purchase.name }} + + {{ transaction.buyer }} paid for {{ transaction.name }} - {{ purchase.created_at|naturaltime }} + {{ transaction.created_at|naturaltime }} - {{ purchase.price }} + {{ transaction.price }}
+ {% elif typ == 'liquidation' %} +
+
+ {{ transaction.creditor.username }} + + {{ transaction.creditor }} gave to {{ transaction.debtor }} + + {{ transaction.debtor.username }} + + {{ transaction.created_at|naturaltime }} + +
+ {{ transaction.description }} +
+ + {{ transaction.amount }} + +
+
+ {% endif %} {% endfor %}
diff --git a/payshare/purchases/views.py b/payshare/purchases/views.py index f8a976f..6ee4425 100644 --- a/payshare/purchases/views.py +++ b/payshare/purchases/views.py @@ -8,6 +8,7 @@ from payshare.purchases.models import Collective from payshare.purchases.models import Purchase +from payshare.purchases.models import Liquidation from payshare.purchases.forms import PurchaseForm from payshare.purchases.forms import LiquidationForm @@ -20,7 +21,7 @@ def index(request): for member in members: purchases.extend(Purchase.objects.filter(collective=collective, buyer=member)) - purchases = sorted(purchases, key=lambda p: p.created_at, reverse=True) + liquidations = list(Liquidation.objects.filter(collective=collective)) overall_purchased = sum([purchase.price for purchase in purchases]) per_member = float(overall_purchased) / float(len(members)) @@ -29,16 +30,35 @@ def index(request): for member in members: member_purchased = sum([purchase.price for purchase in purchases if purchase.buyer == member]) - has_to_pay = per_member - float(member_purchased) + + credit = sum([liq.amount for liq in liquidations + if liq.creditor == member]) + debt = sum([liq.amount for liq in liquidations + if liq.debtor == member]) + has_to_pay = ( + per_member - + float(member_purchased) - + float(credit) + + float(debt) + ) + balance = has_to_pay * -1 if balance == 0: # Remove '-' from the display. balance = 0 member_summary[member] = balance + transactions = purchases + liquidations + transactions = sorted(transactions, key=lambda t: t.created_at, + reverse=True) + transactions = [ + ("purchase" if isinstance(t, Purchase) else "liquidation", t) + for t in transactions + ] + return render(request, "index.html", { "collective": collective, "members": members, - "purchases": purchases, + "transactions": transactions, "overall_purchased": overall_purchased, "member_summary": member_summary, "purchase_form": PurchaseForm(initial={"collective": collective}), diff --git a/todo.org b/todo.org index 03e30fc..5933f6d 100644 --- a/todo.org +++ b/todo.org @@ -1,6 +1,6 @@ -* TODO [0/2] +* TODO [1/5] - - [ ] consider liquidations when calculating balance + - [X] consider liquidations when calculating balance - [ ] implement modal to add purchase - [ ] implement modal to add liquidation - [ ] filtering for member, price, time, title, ... From 84c68f432ce2df8a3dfe702d33c2532df05bc06b Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Tue, 2 May 2017 20:07:11 +0200 Subject: [PATCH 021/343] Specify collective to visit via the uuid in the url. --- payshare/purchases/templates/index.html | 2 +- payshare/purchases/views.py | 11 ++++++++--- payshare/urls.py | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html index aaf1467..646a7fb 100644 --- a/payshare/purchases/templates/index.html +++ b/payshare/purchases/templates/index.html @@ -124,7 +124,7 @@
-

{{ collective.name }}

+

{{ collective.name }}

Created at {{ collective.created_at|date }}
{{ collective.membership_set.count|apnumber|capfirst }} Users
diff --git a/payshare/purchases/views.py b/payshare/purchases/views.py index 6ee4425..f5dda59 100644 --- a/payshare/purchases/views.py +++ b/payshare/purchases/views.py @@ -3,7 +3,8 @@ # from django.contrib.auth.models import User from django.shortcuts import render -# from django.http import HttpResponse +from django.http import HttpResponse +from django.core.exceptions import ValidationError # from moneyed import Money, EUR from payshare.purchases.models import Collective @@ -13,8 +14,12 @@ from payshare.purchases.forms import LiquidationForm -def index(request): - collective = Collective.objects.first() +def index(request, uuid): + try: + collective = Collective.objects.get(key=uuid) + except (Collective.DoesNotExist, ValidationError): + return HttpResponse("This does not exist :(", status=404) + members = [ms.member for ms in collective.membership_set.all()] purchases = [] diff --git a/payshare/urls.py b/payshare/urls.py index e52d9a0..c9d42a0 100644 --- a/payshare/urls.py +++ b/payshare/urls.py @@ -20,8 +20,8 @@ # from payshare.purchases.views import purchase_create urlpatterns = [ - url(r"^$", index, name="index"), url(r"^admin/", admin.site.urls), + url(r"^(?P[^/]+)", index, name="index"), # url(r"^purchase/create/$", purchase_create, name="purchase-create"), # url(r"^purchase/create/$", PurchaseCreateView.as_view(), # name="purchase-create"), From 42bb9c47a820b364a78eb9c55eed1c2fd413d09a Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Tue, 2 May 2017 23:28:42 +0200 Subject: [PATCH 022/343] Change initial viewport scale for mobile. --- payshare/purchases/templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html index 646a7fb..9d4c721 100644 --- a/payshare/purchases/templates/index.html +++ b/payshare/purchases/templates/index.html @@ -1,7 +1,7 @@ - + Payshare From 04129f61bd7f46827fa99594c2522115aa3f0438 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 2 May 2017 21:30:00 +0000 Subject: [PATCH 023/343] Extend admin list display of collectives. --- payshare/purchases/admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/payshare/purchases/admin.py b/payshare/purchases/admin.py index 78c55e9..a486133 100644 --- a/payshare/purchases/admin.py +++ b/payshare/purchases/admin.py @@ -10,7 +10,8 @@ class CollectiveAdmin(admin.ModelAdmin): - readonly_fields = ["key"] + readonly_fields = ("key",) + list_display = ("name", "key", "id",) admin.site.register(Collective, CollectiveAdmin) From 299c638173908078befe45dfaec8848b187cca03 Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Mon, 8 May 2017 19:02:45 +0200 Subject: [PATCH 024/343] Limit only member cards with fixed width. --- payshare/purchases/templates/index.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html index 9d4c721..5ac6603 100644 --- a/payshare/purchases/templates/index.html +++ b/payshare/purchases/templates/index.html @@ -44,7 +44,6 @@ } .card { - width: 200px; display: flex; justify-content: space-around; margin: 1em; @@ -56,6 +55,10 @@ transform: scale(1.08); } + .card.member { + width: 200px; + } + .purchase-overall { margin-bottom: 1em; font-weight: bold; @@ -82,7 +85,6 @@ } .card.purchase { - min-width: 50%; margin: 3px 10px 3px 10px; box-shadow: none; border-color: #DDD; @@ -147,7 +149,7 @@

{{ collective.name }}

{% for member in members %} -
+
{{ member.username }} From 4d1e4480f9b5d66648e144db5f14dd9bfcd68ae3 Mon Sep 17 00:00:00 2001 From: Christoph Buelter Date: Mon, 8 May 2017 20:00:09 +0200 Subject: [PATCH 025/343] Add a basic form to add purchases. --- payshare/purchases/forms.py | 16 ------ payshare/purchases/templates/index.html | 74 +++++++++++++++++++------ payshare/purchases/views.py | 47 ++++++++-------- payshare/urls.py | 6 +- 4 files changed, 81 insertions(+), 62 deletions(-) delete mode 100644 payshare/purchases/forms.py diff --git a/payshare/purchases/forms.py b/payshare/purchases/forms.py deleted file mode 100644 index f0c73d2..0000000 --- a/payshare/purchases/forms.py +++ /dev/null @@ -1,16 +0,0 @@ -from django import forms - -from payshare.purchases.models import Purchase -from payshare.purchases.models import Liquidation - - -class PurchaseForm(forms.ModelForm): - class Meta: - model = Purchase - fields = ["name", "price", "buyer", "collective"] - - -class LiquidationForm(forms.ModelForm): - class Meta: - model = Liquidation - fields = ["amount", "debtor", "creditor", "collective"] diff --git a/payshare/purchases/templates/index.html b/payshare/purchases/templates/index.html index 5ac6603..2b05dd8 100644 --- a/payshare/purchases/templates/index.html +++ b/payshare/purchases/templates/index.html @@ -16,6 +16,10 @@ background-color: #EEE; } + .hidden { + display: none; + } + .jumbotron { background-color: white; } @@ -85,6 +89,7 @@ } .card.purchase { + width: 90%; margin: 3px 10px 3px 10px; box-shadow: none; border-color: #DDD; @@ -133,11 +138,11 @@

{{ collective.name }}

{{ collective.purchase_set.count|apnumber|capfirst }} Purchases
{{ collective.liquidation_set.count|apnumber|capfirst }} Liquidations
- - @@ -176,18 +181,46 @@

{{ member.username }}