diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8432ce61..b43e8653 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -21,6 +21,11 @@ You are more than welcome to contribute to the system. This guide documents how - To create a super user for the admin interface you can run `docker-compose run web ./manage.py createsuperuser` +- To see the emails generated by the system (Payment confirmations etc.) run + `docker-compose run web ./manage.py render_emails` + This will create a `generated_emails` folder with the html and text version + of the emails. + ## Primary Frameworks/Systems used - [Django][django]: The base web framework used. The link is to their great diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8bc4b5eb..d840d55f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,3 +40,10 @@ jobs: with: name: UML_diagram.png path: UML + + - name: Create and render emails + run: docker-compose run web ./manage.py render_emails + - uses: actions/upload-artifact@v1 + with: + name: rendered_emails + path: ./generated_emails diff --git a/.gitignore b/.gitignore index de0a3458..9867307b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ members/static/members/css .sass-cache/ public_data.zip test-screens +generated_emails diff --git a/forenings_medlemmer/settings.py b/forenings_medlemmer/settings.py index 4912ead3..6a13deee 100644 --- a/forenings_medlemmer/settings.py +++ b/forenings_medlemmer/settings.py @@ -64,6 +64,7 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = env.str("SECRET_KEY") + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env.bool("DEBUG") if DEBUG: @@ -183,6 +184,7 @@ # Dont keep job logs more than 7 days old DJANGO_CRON_DELETE_LOGS_OLDER_THAN = 7 +QUICKPAY_URL = "https://api.quickpay.net/payments" QUICKPAY_API_KEY = os.environ["QUICKPAY_API_KEY"] QUICKPAY_PRIVATE_KEY = os.environ["QUICKPAY_PRIVATE_KEY"] PAYMENT_ID_PREFIX = env.str("PAYMENT_ID_PREFIX") diff --git a/members/admin/__init__.py b/members/admin/__init__.py index b295d5db..7a896f5d 100644 --- a/members/admin/__init__.py +++ b/members/admin/__init__.py @@ -19,12 +19,14 @@ Union, Volunteer, Member, + Membership, Activity, ActivityInvite, ActivityParticipant, Family, EmailItem, Payment, + PayableItem, Equipment, EquipmentLoan, EmailTemplate, @@ -38,6 +40,8 @@ from .member_admin import MemberAdmin from .payment_admin import PaymentAdmin from .activity_admin import ActivityAdmin +from .membership_admin import MembershipAdmin +from .payable_item_admin import PayableItemAdmin admin.site.site_header = "Coding Pirates Medlemsdatabase" admin.site.index_title = "Afdelings admin" @@ -47,8 +51,10 @@ admin.site.register(Union, UnionAdmin) admin.site.unregister(User) admin.site.register(User, UserAdmin) +admin.site.register(PayableItem, PayableItemAdmin) admin.site.register(Person, PersonAdmin) admin.site.register(Member, MemberAdmin) +admin.site.register(Membership, MembershipAdmin) admin.site.register(Payment, PaymentAdmin) admin.site.register(Activity, ActivityAdmin) admin.site.register(EmailTemplate) diff --git a/members/admin/membership_admin.py b/members/admin/membership_admin.py new file mode 100644 index 00000000..92a310f5 --- /dev/null +++ b/members/admin/membership_admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin + + +class MembershipAdmin(admin.ModelAdmin): + list_display = ("person", "union", "sign_up_date") + list_filter = ("union", "sign_up_date", "sign_up_date") + readonly_fields = ["person", "union", "sign_up_date"] + fieldsets = [ + ("Medlemskab", {"fields": ("person", "union", "sign_up_date")}), + ] + + def has_delete_permission(self, request, obj=None): + return False + + def has_add_permission(self, request, obj=None): + return False diff --git a/members/admin/payable_item_admin.py b/members/admin/payable_item_admin.py new file mode 100644 index 00000000..e647ca8f --- /dev/null +++ b/members/admin/payable_item_admin.py @@ -0,0 +1,31 @@ +from django.contrib import admin + + +class PayableItemAdmin(admin.ModelAdmin): + list_display = ( + "added", + "person", + "refunded", + "quick_pay_id", + "accepted", + "amount_ore", + ) + list_filter = ("refunded", "accepted") + readonly_fields = [ + "added", + "person", + "refunded", + "quick_pay_id", + "accepted", + "amount_ore", + ] + fieldsets = [ + ("Data", {"fields": ("person", "added", "amount_ore", "quick_pay_id")}), + ("Status", {"fields": ("refunded", "accepted",)}), + ] + + def has_delete_permission(self, request, obj=None): + return False + + def has_add_permission(self, request, obj=None): + return False diff --git a/members/admin/union_admin.py b/members/admin/union_admin.py index 5cc80e07..245900bd 100644 --- a/members/admin/union_admin.py +++ b/members/admin/union_admin.py @@ -56,6 +56,13 @@ def get_queryset(self, request): ) }, ), + ( + "Kontingent", + { + "fields": ("membership_price_ore",), + "description": "Sæt kontingent for at blive medlem af jeres forening", + }, + ), ( "Info", { diff --git a/members/forms/__init__.py b/members/forms/__init__.py index 688ba0a6..1f3a04b0 100644 --- a/members/forms/__init__.py +++ b/members/forms/__init__.py @@ -4,12 +4,14 @@ from .admin_signup_form import adminSignupForm from .activity_signup_form import ActivitySignupForm from .activity_invite_decline_form import ActivivtyInviteDeclineForm +from .membership_form import MembershipForm __all__ = [ ActivivtyInviteDeclineForm, ActivitySignupForm, PersonForm, signupForm, + MembershipForm, vol_signupForm, adminSignupForm, ] diff --git a/members/forms/membership_form.py b/members/forms/membership_form.py new file mode 100644 index 00000000..f16c5e5f --- /dev/null +++ b/members/forms/membership_form.py @@ -0,0 +1,22 @@ +from django import forms +from members.models import Person, Union +from members.models import Membership + + +class MembershipForm(forms.Form): + def __init__(self, family_members, *args, **kwargs): + super(MembershipForm, self).__init__(*args, **kwargs) + self.family_members = family_members + self.fields["person"].queryset = Person.objects.filter(pk__in=family_members) + self.fields["person"].initial = 1 + + # TODO exclude closed unions and union where is member + self.fields["union"].queryset = Union.objects.all() + + person = forms.ModelChoiceField(Person.objects.none()) + union = forms.ModelChoiceField(Union.objects.none(), label="Forening") + + def clean(self): + Membership.can_be_member_validator( + self.cleaned_data["person"], self.cleaned_data["union"] + ) diff --git a/members/management/commands/render_emails.py b/members/management/commands/render_emails.py new file mode 100644 index 00000000..9572bc49 --- /dev/null +++ b/members/management/commands/render_emails.py @@ -0,0 +1,28 @@ +import os + +from django.core.management.base import BaseCommand + +from members.tests.factories import PayableItemFactory +from members.models import PayableItem + + +email_dir = "generated_emails" + + +class Command(BaseCommand): + help = "Renders emails with test data" + + def handle(self, *args, **options): + if not os.path.exists(email_dir): + os.makedirs(email_dir) + + _write_payment_confirmation() + + +def _write_payment_confirmation(): + payment = PayableItemFactory.build() + html, text = PayableItem._render_payment_confirmation(payment) + with open(f"{email_dir}/payment_confirmed_email.html", "w+") as htmlFile: + htmlFile.write(html) + with open(f"{email_dir}/payment_confirmed_email.txt", "w+") as txtFile: + txtFile.write(text) diff --git a/members/management/commands/send_emails.py b/members/management/commands/send_emails.py new file mode 100644 index 00000000..4e2a50af --- /dev/null +++ b/members/management/commands/send_emails.py @@ -0,0 +1,10 @@ +from django.core.management.base import BaseCommand + +from members.models import PayableItem + + +class Command(BaseCommand): + help = "Send emails in queue" + + def handle(self, *args, **options): + PayableItem.send_all_payment_confirmations() diff --git a/members/migrations/0023_auto_20200714_2338.py b/members/migrations/0023_auto_20200714_2338.py new file mode 100644 index 00000000..b6476ff4 --- /dev/null +++ b/members/migrations/0023_auto_20200714_2338.py @@ -0,0 +1,141 @@ +# Generated by Django 3.0.8 on 2020-07-14 21:38 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import members.models.payable_item + + +class Migration(migrations.Migration): + + dependencies = [ + ("members", "0022_merge_20200526_1130"), + ] + + operations = [ + migrations.CreateModel( + name="Membership", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("sign_up_date", models.DateField(verbose_name="Opskrivningsdato")), + ( + "person", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="members.Person" + ), + ), + ], + options={ + "verbose_name": "Foreningsmedlemskab", + "verbose_name_plural": "Foreningsmedlemskaber", + "ordering": ["union"], + }, + ), + migrations.AddField( + model_name="union", + name="closed", + field=models.DateField(blank=True, null=True, verbose_name="Lukket"), + ), + migrations.AddField( + model_name="union", + name="membership_price_ore", + field=models.IntegerField( + default=7500, verbose_name="Medlemskontingent i ører" + ), + ), + migrations.CreateModel( + name="PayableItem", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "added", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="Tilføjet" + ), + ), + ( + "refunded", + models.DateTimeField( + blank=True, null=True, verbose_name="Refunderet" + ), + ), + ("amount_ore", models.IntegerField(verbose_name="Beløb i øre")), + ( + "quick_pay_order_id", + models.CharField( + default=members.models.payable_item._set_quickpay_order_id, + max_length=20, + unique=True, + verbose_name="Quick Pay order ID", + ), + ), + ( + "accepted", + models.BooleanField(default=False, verbose_name="Accepteret"), + ), + ( + "processed", + models.BooleanField(default=False, verbose_name="Processed"), + ), + ( + "confirmation_sent", + models.BooleanField( + default=False, verbose_name="Bekræftelse sendt" + ), + ), + ("quick_pay_id", models.IntegerField(verbose_name="Quick Pay ID")), + ( + "_payment_link", + models.URLField( + blank=True, null=True, verbose_name="Betalingslink" + ), + ), + ( + "membership", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="payment", + to="members.Membership", + ), + ), + ( + "person", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="payments", + to="members.Person", + ), + ), + ], + options={ + "verbose_name": "Betaling", + "verbose_name_plural": "Betalinger", + "ordering": ["added"], + }, + ), + migrations.AddField( + model_name="membership", + name="union", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="members.Union" + ), + ), + ] diff --git a/members/models/__init__.py b/members/models/__init__.py index be4497c5..92e2eb87 100644 --- a/members/models/__init__.py +++ b/members/models/__init__.py @@ -7,7 +7,6 @@ import members.models.activityinvite import members.models.activityparticipant - import members.models.department import members.models.emailitem import members.models.emailtemplate @@ -41,6 +40,7 @@ from .equipmentloan import EquipmentLoan from .family import Family from .member import Member +from .membership import Membership from .notification import Notification from .payment import Payment from .person import Person @@ -48,7 +48,8 @@ from .volunteer import Volunteer from .waitinglist import WaitingList from .zipcoderegion import ZipcodeRegion - +from .payable_item import PayableItem +from .quickpaytransaction import QuickpayTransaction from .statistics import gatherDayliStatistics @@ -62,13 +63,16 @@ DailyStatisticsRegion, DailyStatisticsUnion, Department, + PayableItem, EmailItem, EmailTemplate, Equipment, EquipmentLoan, Family, gatherDayliStatistics, + QuickpayTransaction, Member, + Membership, Notification, Payment, Person, diff --git a/members/models/membership.py b/members/models/membership.py new file mode 100644 index 00000000..d67c151c --- /dev/null +++ b/members/models/membership.py @@ -0,0 +1,39 @@ +from django.db import models +from django.core.exceptions import ValidationError +from datetime import date + + +class Membership(models.Model): + class Meta: + verbose_name = "Foreningsmedlemskab" + verbose_name_plural = "Foreningsmedlemskaber" + ordering = ["union"] + + union = models.ForeignKey("Union", on_delete=models.CASCADE) + person = models.ForeignKey("Person", on_delete=models.CASCADE) + sign_up_date = models.DateField("Opskrivningsdato") + + def clean(self): + self.sign_up_date = ( + date.today() if self.sign_up_date is None else self.sign_up_date + ) + self.can_be_member_validator(self.person, self.union, self.sign_up_date.year) + + def save(self, *args, **kwargs): + self.clean() + super(Membership, self).save(*args, **kwargs) + + def __str__(self): + return f"Medlemskab: {self.person} : {self.union}" + + @staticmethod + def can_be_member_validator(person, union, year=None): + old_memberships = [ + membership.sign_up_date.year + for membership in Membership.objects.filter(person=person, union=union) + ] + year = date.today().year if year is None else year + if year in old_memberships: + raise ValidationError( + f"{person} er allerede medlem af foreningen {union} i år {year}" + ) diff --git a/members/models/payable_item.py b/members/models/payable_item.py new file mode 100644 index 00000000..f7e14b0f --- /dev/null +++ b/members/models/payable_item.py @@ -0,0 +1,207 @@ +import requests +import json +from django.core.mail import EmailMultiAlternatives + +from django.db import models +from django.utils import timezone +from django.core.exceptions import ValidationError +from django.conf import settings +from members.models import quickpaytransaction +from django.urls import reverse +from django.template.loader import get_template + + +def _set_quickpay_order_id(): + if settings.PAYMENT_ID_PREFIX == "pro": + id = ( + PayableItem.objects.all().count() + + quickpaytransaction.QuickpayTransaction.objects.all().count() + + 1 + ) + return f"prod-{id}" + else: + return f"{settings.PAYMENT_ID_PREFIX}{timezone.now().timestamp()}" + + +headers = { + "accept-version": "v10", + "content-type": "application/json", +} +auth = ("", settings.QUICKPAY_API_KEY) + + +class PayableItem(models.Model): + class Meta: + verbose_name_plural = "Betalinger" + verbose_name = "Betaling" + ordering = ["added"] + + person = models.ForeignKey( + "Person", on_delete=models.PROTECT, related_name="payments" + ) + added = models.DateTimeField("Tilføjet", default=timezone.now) + refunded = models.DateTimeField("Refunderet", null=True, blank=True) + amount_ore = models.IntegerField("Beløb i øre") + # Payable items, at least one of them should not be null. + membership = models.ForeignKey( + "Membership", + on_delete=models.PROTECT, + related_name="payment", + null=True, + blank=True, + ) + quick_pay_order_id = models.CharField( + "Quick Pay order ID", + max_length=20, + unique=True, + default=_set_quickpay_order_id, + ) + accepted = models.BooleanField("Accepteret", default=False) + processed = models.BooleanField("Processed", default=False) + confirmation_sent = models.BooleanField("Bekræftelse sendt", default=False) + quick_pay_id = models.IntegerField("Quick Pay ID") + _payment_link = models.URLField("Betalingslink", null=True, blank=True) + + __original_quick_pay_order_id = None + __original_quick_pay_id = None + + def __init__(self, *args, **kwargs): + super(PayableItem, self).__init__(*args, **kwargs) + self.clean() + if self.quick_pay_id is None: + self.__original_quick_pay_order_id = self.quick_pay_order_id + self.quick_pay_id = self.__create_quickpay_transaction() + self.__original_quick_pay_id = self.quick_pay_id + + def clean(self): + if ( + self.__original_quick_pay_id is not None + and self.quick_pay_id != self.__original_quick_pay_id + ): + raise ValidationError(f"{self} tried to change quick_pay_id") + if ( + self.__original_quick_pay_order_id is not None + and self.quick_pay_order_id != self.__original_quick_pay_order_id + ): + raise ValidationError(f"{self} tried to change quick_pay_order_id") + if self.membership is None: + raise ValidationError(f"{self} does not have any membership") + if self.amount_ore < 100: + raise ValidationError( + f"{self.amount_ore} is below 1kr, rember to specify price in øre" + ) + + def save(self, *args, **kwargs): + self.clean() + super(PayableItem, self).save(*args, **kwargs) + + def __create_quickpay_transaction(self): + response = requests.post( + settings.QUICKPAY_URL, + headers=headers, + auth=auth, + data=json.dumps( + { + "order_id": self.quick_pay_order_id, + "currency": "dkk", + "text_on_statement": "Coding Pirates", + } + ), + ) + if response.status_code != 201: + raise requests.HTTPError( + f"Quick pay payment failed, see:\n {response.text}" + ) + return response.json()["id"] + + def __str__(self): + return ( + f"Betaling: {self.person} på {self.show_amount()}kr, for {self.get_item()}" + ) + + def get_link(self, base_url=None, continue_page=None): + if self._payment_link is not None: + return self._payment_link + + data = { + "amount": self.amount_ore, + "language": "da", + "auto_capture": True, + } + if base_url is not None: + data["callback_url"] = f"{base_url}{reverse('quickpay_callback_new')}" + data["continue_url"] = ( + "" if continue_page is None else f"{base_url}{reverse(continue_page)}" + ) + response = requests.put( + f"{settings.QUICKPAY_URL}/{self.quick_pay_id}/link", + auth=auth, + headers=headers, + data=json.dumps(data), + ) + if response.status_code != 200: + raise requests.HTTPError(f"Quick pay link failed, see:\n {response.text}") + self._payment_link = response.json()["url"] + self.save() + return self._payment_link + + def get_status(self): + if self.refunded is not None: + return "refunded" + if self.accepted: + return "accepted" + response = requests.get( + f"{settings.QUICKPAY_URL}/{self.quick_pay_id}", auth=auth, headers=headers, + ) + if response.status_code != 200: + raise requests.HTTPError( + f"Quick pay get payment info failed, see:\n {response.text}" + ) + resp = response.json() + self.processed = resp["state"] == "processed" + self.accepted = resp["accepted"] + self.save() + if self.accepted: + return "accepted" + else: + return resp["state"] + + def get_item(self): + if self.membership is not None: + return self.membership + + def get_item_name(self): + if self.membership is not None: + return "Medlemsskab" + + def show_amount(self): + return f"{(self.amount_ore / 100):,.2f}".replace(".", ",") + + def send_payment_confirmation(self): + if self.get_status() == "accepted" and not self.confirmation_sent: + subject = "Betalingsbekræftelse" + html_content, text_content = PayableItem._render_payment_confirmation(self) + msg = EmailMultiAlternatives( + subject, text_content, settings.DEFAULT_FROM_EMAIL, [self.person.email], + ) + msg.attach_alternative(html_content, "text/html") + self.confirmation_sent = True + self.save() + return msg.send(fail_silently=False) + + @staticmethod + def _render_payment_confirmation(payment): + html_template = get_template("members/email/payment_confirm.html") + html = html_template.render({"payment": payment}) + + txt_template = get_template("members/email/payment_confirm.txt") + txt = txt_template.render({"payment": payment}) + + return html, txt + + @staticmethod + def send_all_payment_confirmations(): + return [ + payment.send_payment_confirmation() + for payment in PayableItem.objects.filter(confirmation_sent=False) + ] diff --git a/members/models/quickpaytransaction.py b/members/models/quickpaytransaction.py index 95d70b6e..92678a30 100644 --- a/members/models/quickpaytransaction.py +++ b/members/models/quickpaytransaction.py @@ -4,6 +4,8 @@ from django.urls import reverse from django.utils import timezone +from .payable_item import _set_quickpay_order_id + class QuickpayTransaction(models.Model): payment = models.ForeignKey("Payment", on_delete=models.PROTECT) @@ -24,10 +26,7 @@ class QuickpayTransaction(models.Model): def save(self, *args, **kwargs): """ On creation make quickpay order_id from payment id """ if self.pk is None: - if settings.DEBUG: - self.order_id = f"dev{timezone.now().timestamp()}" - else: - self.order_id = "prod" + "%06d" % self.payment.pk + self.order_id = _set_quickpay_order_id() return super(QuickpayTransaction, self).save(*args, **kwargs) # method requests payment URL from Quickpay. diff --git a/members/models/union.py b/members/models/union.py index 52fc4fab..96ba04f4 100644 --- a/members/models/union.py +++ b/members/models/union.py @@ -58,6 +58,8 @@ class Meta: "Sæt kryds hvis I har konto hos hovedforeningen (og ikke har egen bankkonto).", default=True, ) + closed = models.DateField("Lukket", blank=True, null=True) + membership_price_ore = models.IntegerField("Medlemskontingent i ører", default=7500) bank_account = models.CharField( "Bankkonto:", max_length=15, diff --git a/members/static/members/js/membership_create.js b/members/static/members/js/membership_create.js new file mode 100644 index 00000000..1a7efa55 --- /dev/null +++ b/members/static/members/js/membership_create.js @@ -0,0 +1,33 @@ +const base_url = window.location.href + .split("/") + .slice(0, 3) + .join("/"); + +let unions = fetch(`${base_url}/graphql`, { + method: "POST", + headers: { + "content-type": "application/json" + }, + body: '{"query":"{ unions {id membershipPriceOre }}"}' +}) + .then(res => res.json()) + .then(res => { + unions = res.data["unions"]; + }) + .catch(err => { + console.log(err); // TODO: error handling + }); + + +document.addEventListener("DOMContentLoaded", event => { + // Set year + document.getElementById("year").innerText = new Date().getFullYear() + + + // Sets price according to selected union + document.getElementById("id_union").addEventListener("change", event => { + document.getElementById("price").innerText = unions.filter( + union => union.id === event.target.value + )[0]["membershipPriceOre"] / 100; + }); +}); diff --git a/members/static/members/sass/_definitions.scss b/members/static/members/sass/_definitions.scss index bd2db737..58daea05 100644 --- a/members/static/members/sass/_definitions.scss +++ b/members/static/members/sass/_definitions.scss @@ -1,7 +1,7 @@ $primary-color: #00aeef; $primary-color-muted: #00a0e0; $accent-color: #ff9900; -$accent-color-muted: #ed9224; +$accent-color-muted: #e5841c; $danger-color: #dc3545; $danger-color-muted: #a01525; $success-color: #28a745; diff --git a/members/static/members/sass/forms.scss b/members/static/members/sass/forms.scss new file mode 100644 index 00000000..d71b9f0e --- /dev/null +++ b/members/static/members/sass/forms.scss @@ -0,0 +1,31 @@ +@use "_definitions"; +@use "buttons"; + +form { + select { + width: 90%; + display: block; + margin: 0 auto; + } + label { + margin-top: 10px; + margin-right: 5px; + margin-left: 15px; + font-weight: 800; + } + + input[type="submit"] { + display: block; + @extend .button; + } +} + +.errorlist { + li { + @extend %rounded-rect; + background-color: _definitions.$danger-color; + font-weight: 600; + padding: 5px; + width: 90%; + } +} diff --git a/members/static/members/sass/header.scss b/members/static/members/sass/header.scss index 4f019aa7..74c31727 100644 --- a/members/static/members/sass/header.scss +++ b/members/static/members/sass/header.scss @@ -39,7 +39,7 @@ nav { } #login-logout { text-align: right; - flex-grow: 2; + flex-grow: 2 } .inactive { diff --git a/members/static/members/sass/main.scss b/members/static/members/sass/main.scss index 16912492..ef01a2e8 100644 --- a/members/static/members/sass/main.scss +++ b/members/static/members/sass/main.scss @@ -9,5 +9,6 @@ @use "style"; @use "table"; @use "tab"; +@use "forms"; @use "department_list"; -@import url("https://fonts.googleapis.com/css?family=Bungee|Lato|Pirata+One&display=swap"); \ No newline at end of file +@import url("https://fonts.googleapis.com/css?family=Bungee|Lato|Pirata+One&display=swap"); diff --git a/members/templates/members/email/payment_confirm.html b/members/templates/members/email/payment_confirm.html new file mode 100644 index 00000000..06fc55d5 --- /dev/null +++ b/members/templates/members/email/payment_confirm.html @@ -0,0 +1,471 @@ + + + + + + Coding Pirates Betalingsbekræftelse + + + + + + + + + +
+
+ + + + +
+ + +
+ + + + + +
+
+ + + + +
+ + + + + + + +
+ Betalingbekræftelse +
+ Hi {{payment.person}}
+ Du har den {{payment.added}} betalt for følgende + ved Coding Pirates +
+ + + + + + + + + + + + + + + + + +
+ Person + + Købt + + Beløb +
+ {{payment.get_item.person}} + + {{payment.get_item_name}} + + {{payment.show_amount}}kr +
+ + + + + +
+ +
+ + + + + + + + +
+ Med venlig hilsen +
+ Coding Pirates +
+
+
+
+ + + + + +
+
+ + + + +
+ + + + +
+ Skriv til os hvis du har nogle spørgsmål + til overstående.
+ kontakt@codingpirates.dk + +
+
+
+
+
+
+ + diff --git a/members/templates/members/email/payment_confirm.txt b/members/templates/members/email/payment_confirm.txt new file mode 100644 index 00000000..f819b485 --- /dev/null +++ b/members/templates/members/email/payment_confirm.txt @@ -0,0 +1,15 @@ +Betalingbekræftelse + +Hi {{payment.person}} +Du har den {{payment.added}} betalt for følgende ved Coding Pirates + +Person: {{payment.get_item.person}} +Købt: {{payment.get_item_name}} +Pris: {{payment.show_amount}}kr + +Se mere (members.codingpirates.dk/payments) + +Med venlig hilsen Coding Pirates + +Skriv til os hvis du har nogle spørgsmål til overstående. +kontakt@codingpirates.dk (mailto:kontakt@codingpirates.dk) diff --git a/members/templates/members/entry_page.html b/members/templates/members/entry_page.html index 7cc7bf4d..f2ccfb12 100644 --- a/members/templates/members/entry_page.html +++ b/members/templates/members/entry_page.html @@ -2,43 +2,59 @@ {% block content %} {% if user.is_authenticated %} -
-
- -
-

Jeres familie

- - Se familie - -
+
+
+ +
+

Jeres familie

+ + Se familie + +
+
+
+ +
+

Tilmeld Afdelinger

+ + Afdelinger + +
+
+
+ +
+

Tilmeld arrangementer

+ + Arrangementer + +
+
+
+ +
+

Medlemskaber

+ + Se/få medlemsskab + +
+
+
+ +
+

Betalinger

+ + Se dine betalinger + +
+
-
- -
-

Tilmeld Afdelinger

- - Afdelinger - -
-
-
- -
-

Tilmeld arrangementer

- - Arrangementer - -
-
-
- -

- For yderligere hjælp med at bruge denne side - se på - "Sådan bruger du vores - medlemssystem" eller skriv til os på - kontakt@codingpirates.dk. -

- +

+ For yderligere hjælp med at bruge denne side - se på + "Sådan bruger du vores + medlemssystem" eller skriv til os på + kontakt@codingpirates.dk. +

{% else %}

Tilmelding til Coding Pirates

diff --git a/members/templates/members/family_details.html b/members/templates/members/family_details.html index 2c764e78..71c558c0 100644 --- a/members/templates/members/family_details.html +++ b/members/templates/members/family_details.html @@ -32,7 +32,7 @@

Personer

- + {% if family.person_set.count > 0 %} diff --git a/members/templates/members/header.html b/members/templates/members/header.html index 4f5981ea..8fec5ecb 100644 --- a/members/templates/members/header.html +++ b/members/templates/members/header.html @@ -8,6 +8,8 @@ FamilieAfdelingerArrangementer + Medlemskaber + BetalingerLog ud {% else %} Tilmeld barn diff --git a/members/templates/members/memberships.html b/members/templates/members/memberships.html new file mode 100644 index 00000000..d8c080d0 --- /dev/null +++ b/members/templates/members/memberships.html @@ -0,0 +1,49 @@ +{% extends 'members/base.html' %} {% load static %} +{% block content %} + +

Medlemskaber

+

+ Coding Pirates består af {{ unions | length}} foreninger. Hver forening har + sin egen bestyrelse. For at blive medlem at en forening skal man betale + kontingent, hver forening sætter sin egen kontingent. Det er et krav at være + medlem for at være stemmeberetiget til en generalforsamling. + Nogle aktiviteter i Coding Pirates, f.eks klubaftner, har prisen på + medlemsskab indregnet. +

+

+ På denne side kan du se dine medlemsskaber og melde dig ind i foreninger. +

+ +
+

Bliv medlem

+
+ {% csrf_token %} + {{ form }} + + + + +
+ +{% if current_memberships %} +
+

Mine medlemsskaber

+
+ + + + + + + {% for membership in current_memberships %} + + + + + + {% endfor %} + +
NavnForeningÅr
{{membership.person}}{{membership.union}}{{membership.sign_up_date.year}}
+ +{% endif %} +{% endblock %} diff --git a/members/templates/members/payments.html b/members/templates/members/payments.html new file mode 100644 index 00000000..130a4d8c --- /dev/null +++ b/members/templates/members/payments.html @@ -0,0 +1,38 @@ +{% extends 'members/base.html' %} {% load static %} +{% block content %} + +

Betalinger

+

+ På denne side kan du se et oversigt over dine betalinger, hvis en betaling + mangler er den markeret med rød. +

+ + + + + + + + + {% for payment in payments %} + + + + + + + {% endfor %} + +
DatoKøbtBeløbStatus
{{payment.added |date:'d-m-Y '}}{{payment.get_item}}{{payment.show_amount}} kr + {% if payment.get_status == "accepted" %} + Betalt + {% else %} + + Ikke betalt + + {% endif %} +
+ + + +{% endblock %} diff --git a/members/tests/factories/__init__.py b/members/tests/factories/__init__.py index a35602eb..7a6162e1 100644 --- a/members/tests/factories/__init__.py +++ b/members/tests/factories/__init__.py @@ -9,8 +9,11 @@ from .activity_participant_factory import ActivityParticipantFactory from .payment_factory import PaymentFactory from .volunteer_factory import VolunteerFactory +from .membership_factory import MembershipFactory +from .payable_item_factory import PayableItemFactory __all__ = [ + PayableItemFactory, ActivityParticipantFactory, PaymentFactory, AddressFactory, @@ -22,4 +25,5 @@ ActivityFactory, MemberFactory, VolunteerFactory, + MembershipFactory, ] diff --git a/members/tests/factories/membership_factory.py b/members/tests/factories/membership_factory.py new file mode 100644 index 00000000..7ec7b3f6 --- /dev/null +++ b/members/tests/factories/membership_factory.py @@ -0,0 +1,13 @@ +from factory import DjangoModelFactory, SubFactory +from members.models import Membership + +from members.tests.factories.union_factory import UnionFactory +from members.tests.factories.person_factory import PersonFactory + + +class MembershipFactory(DjangoModelFactory): + class Meta: + model = Membership + + union = SubFactory(UnionFactory) + person = SubFactory(PersonFactory) diff --git a/members/tests/factories/payable_item_factory.py b/members/tests/factories/payable_item_factory.py new file mode 100644 index 00000000..d8f47611 --- /dev/null +++ b/members/tests/factories/payable_item_factory.py @@ -0,0 +1,15 @@ +from factory import DjangoModelFactory, SubFactory +from members.models import PayableItem +from factory.fuzzy import FuzzyInteger + +from members.tests.factories.membership_factory import MembershipFactory +from members.tests.factories.person_factory import PersonFactory + + +class PayableItemFactory(DjangoModelFactory): + class Meta: + model = PayableItem + + person = SubFactory(PersonFactory) + amount_ore = FuzzyInteger(7500, 200000) + membership = SubFactory(MembershipFactory) diff --git a/members/tests/test_functional/test_admin_can_load.py b/members/tests/test_functional/test_admin_can_load.py index 54e72f67..3c5592bd 100644 --- a/members/tests/test_functional/test_admin_can_load.py +++ b/members/tests/test_functional/test_admin_can_load.py @@ -1,13 +1,16 @@ -import socket import os -from django.contrib.staticfiles.testing import StaticLiveServerTestCase +import socket + from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities -from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + from django.contrib.auth.models import User +from django.contrib.staticfiles.testing import StaticLiveServerTestCase from members.tests.factories import PersonFactory + """ This test creates a super user and checks that the admin interface can be loaded """ diff --git a/members/tests/test_functional/test_create_family.py b/members/tests/test_functional/test_create_family.py index 144efdf1..1505a34a 100644 --- a/members/tests/test_functional/test_create_family.py +++ b/members/tests/test_functional/test_create_family.py @@ -1,11 +1,14 @@ -import socket import os -from django.contrib.staticfiles.testing import StaticLiveServerTestCase +import socket + from selenium import webdriver -from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +from django.contrib.staticfiles.testing import StaticLiveServerTestCase + """ This test goes to the root signup page and creates a child and parent. diff --git a/members/tests/test_functional/test_create_membership_fail.py b/members/tests/test_functional/test_create_membership_fail.py new file mode 100644 index 00000000..d679287b --- /dev/null +++ b/members/tests/test_functional/test_create_membership_fail.py @@ -0,0 +1,87 @@ +import os +import socket +from datetime import date + +from selenium import webdriver +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from members.models import Membership +from members.tests.factories import FamilyFactory, PersonFactory, UnionFactory + +from .functional_helpers import get_text_contains, log_in + + +""" +This test starts with a family with a child that is a member and the parent +wants to become a member. + +The parent ques to quickpay and leaves the page without entering card details. +Once they go back to our page it is marked at not payed. +""" + + +class SignUpTest(StaticLiveServerTestCase): + host = socket.gethostbyname(socket.gethostname()) + + def setUp(self): + self.email = "parent@example.com" + self.browser = webdriver.Remote( + "http://selenium:4444/wd/hub", DesiredCapabilities.CHROME + ) + self.family = FamilyFactory() + self.person = PersonFactory.create(family=self.family) + self.kid = PersonFactory.create(family=self.family, membertype="CH") + self.union = UnionFactory.create() + Membership.objects.create( + union=self.union, person=self.kid, sign_up_date=date.today() + ) + + def tearDown(self): + if not os.path.exists("test-screens"): + os.mkdir("test-screens") + self.browser.save_screenshot("test-screens/membership_test_final_not_payed.png") + self.browser.quit() + + def test_entry_page(self): + log_in(self, self.person) + + get_text_contains(self.browser, "få medlemsskab")[0].click() + # Check row in membership table has kid's name in it + + self.browser.find_element_by_name("person").click() + + # Click on option in select that has parent name + self.browser.find_element_by_xpath( + f"//*[@id='id_person']/option[text()[contains(., '{self.person}')]]" + ).click() + + # Click on select Union + self.browser.find_element_by_name("union").click() + self.browser.find_element_by_xpath( + f"//*[@id='id_union']/option[text()[contains(., '{self.union}')]]" + ).click() + + # Gå til betaling + self.browser.find_element_by_xpath("//input[@type='submit']").click() + + # Leave quickpay and go back to entry page + self.browser.get(f"{self.live_server_url}") + + try: # Wait to be redirected back from quickpay, worst case i 5 mins + WebDriverWait(self.browser, 60 * 5).until( + EC.title_is("Coding Pirates Medlemssystem") + ) + except Exception: + self.fail("Was not sent back to own page") + + # Go to paymetns views + get_text_contains(self.browser, "Se dine betalinger")[0].click() + + # Klick "ikke betalt" button + self.browser.find_element_by_class_name("button-danger").click() + + # Back at quickpay + self.assertIn("https://payment.quickpay.net", self.browser.current_url) diff --git a/members/tests/test_functional/test_create_membership_success.py b/members/tests/test_functional/test_create_membership_success.py new file mode 100644 index 00000000..03e2b232 --- /dev/null +++ b/members/tests/test_functional/test_create_membership_success.py @@ -0,0 +1,142 @@ +import os +import socket +from datetime import date +import time +from selenium import webdriver +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.keys import Keys +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from django.core import mail +from members.models import Membership, PayableItem +from members.tests.factories import FamilyFactory, PersonFactory, UnionFactory + +from .functional_helpers import get_text_contains, log_in + + +""" +This test starts with a family with a child that is a member and the parent +wants to become a member. + +The parent ques to quickpay and enters a valid card, is sent to the payments +screen. And recvies an email confirmation. +""" + + +class SignUpTest(StaticLiveServerTestCase): + host = socket.gethostbyname(socket.gethostname()) + + def setUp(self): + self.email = "parent@example.com" + self.browser = webdriver.Remote( + "http://selenium:4444/wd/hub", DesiredCapabilities.CHROME + ) + self.family = FamilyFactory() + self.person = PersonFactory.create(family=self.family) + self.kid = PersonFactory.create(family=self.family, membertype="CH") + self.union = UnionFactory.create() + Membership.objects.create( + union=self.union, person=self.kid, sign_up_date=date.today() + ) + + def tearDown(self): + if not os.path.exists("test-screens"): + os.mkdir("test-screens") + self.browser.save_screenshot("test-screens/membership_test_final.png") + self.browser.quit() + + def test_entry_page(self): + log_in(self, self.person) + + get_text_contains(self.browser, "få medlemsskab")[0].click() + # Check row in membership table has kid's name in it + self.assertEqual(self.browser.find_element_by_xpath("//td").text, str(self.kid)) + self.assertGreater( + len(get_text_contains(self.browser, "Mine medlemsskaber")), 0 + ) + + # Click on select person + self.browser.find_element_by_name("person").click() + + # Click on option in select that has parent name + self.browser.find_element_by_xpath( + f"//*[@id='id_person']/option[text()[contains(., '{self.person}')]]" + ).click() + + # Click on select Union + self.browser.find_element_by_name("union").click() + self.browser.find_element_by_xpath( + f"//*[@id='id_union']/option[text()[contains(., '{self.union}')]]" + ).click() + + # Gå til betaling + self.browser.find_element_by_xpath("//input[@type='submit']").click() + + try: # Wait to be redirected to quickpay, worst case i 5 mins + WebDriverWait(self.browser, 60).until( + EC.title_is("Coding Pirates Test Account") + ) + except Exception: + self.fail("Was not sent to quickpay") + + # Enter card number + card_field = self.browser.find_element_by_id("cardnumber") + card_field.send_keys(Keys.NUMPAD1) + card_field.send_keys(Keys.NUMPAD0) + card_field.send_keys(Keys.NUMPAD0) + card_field.send_keys(Keys.NUMPAD0) + + card_field.send_keys(Keys.NUMPAD0) + card_field.send_keys(Keys.NUMPAD6) + card_field.send_keys(Keys.NUMPAD0) + card_field.send_keys(Keys.NUMPAD0) + + card_field.send_keys(Keys.NUMPAD0) + card_field.send_keys(Keys.NUMPAD0) + card_field.send_keys(Keys.NUMPAD0) + card_field.send_keys(Keys.NUMPAD0) + + card_field.send_keys(Keys.NUMPAD0) + card_field.send_keys(Keys.NUMPAD0) + card_field.send_keys(Keys.NUMPAD0) + card_field.send_keys(Keys.NUMPAD2) + # self.browser.find_element_by_id("cardnumber").send_keys("1000 0600 0000 0002") + time.sleep(1) + + # Enter experation month + self.browser.find_element_by_id("expiration-month").send_keys("11") + + # Enter experation year + self.browser.find_element_by_id("expiration-year").send_keys("30") + + # Enter CVS + self.browser.find_element_by_id("cvd").send_keys("208") + self.browser.find_element_by_id("cvd").send_keys(Keys.TAB) + + self.browser.save_screenshot("test-screens/membership_test_card_details.png") + + # Finish payment + self.browser.find_element_by_xpath("//*[@type='submit']").click() + + try: # Wait to be redirected back from quickpay, worst case i 5 mins + WebDriverWait(self.browser, 60).until( + EC.title_is("Coding Pirates Medlemssystem") + ) + except Exception: + self.fail("Was not sent back to own page") + + payment_statuses = self.browser.find_elements_by_xpath( + '//*/td[@data-label="Status"]' + ) + for status in payment_statuses: + self.assertEqual(status.text.strip(), "Betalt") + + # Sent one confirmation email + self.assertEqual(PayableItem.send_all_payment_confirmations(), [1]) + + # Check that we can't send it again + self.assertEqual(PayableItem.send_all_payment_confirmations(), []) + + # Sent to right person + self.assertEqual(mail.outbox[0].to, [self.person.email]) diff --git a/members/tests/test_model_activityparticipant.py b/members/tests/test_model_activityparticipant.py index 338b4acb..f70bb4b3 100644 --- a/members/tests/test_model_activityparticipant.py +++ b/members/tests/test_model_activityparticipant.py @@ -20,6 +20,7 @@ def setUp(self): end_date=datetime.now() + timedelta(days=365), # Has to be long enough to be a season department=self.department, + union=self.department.union, ) self.activity.save() self.assertTrue(self.activity.is_season()) # If this fail increase the end_date diff --git a/members/tests/test_model_membership.py b/members/tests/test_model_membership.py new file mode 100644 index 00000000..3d64364e --- /dev/null +++ b/members/tests/test_model_membership.py @@ -0,0 +1,19 @@ +from django.test import TestCase +from django.core.exceptions import ValidationError +from members.models import Membership +from .factories import UnionFactory, PersonFactory +from datetime import date + + +class TestModelMembership(TestCase): + def test_create_membership_same_year(self): + # Creates two memberships in the same year. + person = PersonFactory.create() + union = UnionFactory.create() + Membership.objects.create( + person=person, union=union, sign_up_date=date(2020, 3, 14) + ) + with self.assertRaises(ValidationError): + Membership.objects.create( + person=person, union=union, sign_up_date=date(2020, 5, 14) + ) diff --git a/members/tests/test_model_payable_item.py b/members/tests/test_model_payable_item.py new file mode 100644 index 00000000..5dc1c476 --- /dev/null +++ b/members/tests/test_model_payable_item.py @@ -0,0 +1,84 @@ +from django.core.exceptions import ValidationError +from requests.exceptions import HTTPError + +from django.test import TestCase +from members.models import PayableItem +from members.models.payable_item import _set_quickpay_order_id +from .factories import PersonFactory, PayableItemFactory + + +class TestPayableItem(TestCase): + def test_set_quickpay_order_id(self): + # Tests that the quickpay order id is set to something uniqe in dev/test + # and count in prod + p1 = PayableItemFactory.create() + with self.settings(PAYMENT_ID_PREFIX="prod"): + id1 = _set_quickpay_order_id() + p2 = PayableItemFactory.create() + with self.settings(PAYMENT_ID_PREFIX="prod"): + id2 = _set_quickpay_order_id() + + self.assertLess(p1.quick_pay_order_id, p2.quick_pay_order_id) + self.assertLess(id1, id2) + + def test_get_cant_create_empty(self): + # Tests that either membership, activty or season is passed + person = PersonFactory.create() + with self.assertRaises(ValidationError): + PayableItem.objects.create(person=person, amount_ore=7500) + + def test_try_to_change_quickpay_id(self): + # Creates a payment item and tries to change the ID which should fail + payment = PayableItemFactory.create() + payment.quick_pay_id = "something else" + with self.assertRaises(ValidationError): + payment.save() + + payment = PayableItemFactory.create() + payment.quick_pay_order_id = "something else" + with self.assertRaises(ValidationError): + payment.save() + + def test_get_payment_link(self): + # Creates a payment item and gets a payment link + payment = PayableItemFactory.create() + link = payment.get_link() + self.assertTrue("https://payment.quickpay.net/payments" in link) + + def test_get_payment_status(self): + # Tests that we can get a payment status + payment = PayableItemFactory.create() + self.assertEqual(payment.get_status(), "initial") + + def test_get_item(self): + # Tests that we can retrive both the paid item and the name. + payment = PayableItemFactory.create() + self.assertEqual(payment.get_item(), payment.membership) + self.assertEqual(payment.get_item_name(), "Medlemsskab") + + def test_show_amount(self): + # Tests that we can retrive both the paid item and the name. + payment = PayableItemFactory.create(amount_ore=10000) + self.assertEqual(payment.show_amount(), "100,00") + + def test_amount_less_than_1kr(self): + # Tests that we can retrive both the paid item and the name. + with self.assertRaises(ValidationError): + PayableItemFactory.create(amount_ore=10) + + def test_cant_connect_to_quickpay(self): + with self.settings(QUICKPAY_URL="http://test.com/"): + with self.assertRaises(HTTPError): + PayableItemFactory.create() + + payment = PayableItemFactory.create() + with self.settings(QUICKPAY_URL="http://test.com/"): + with self.assertRaises(HTTPError): + payment.get_link() + + def test_to_string(self): + payment = PayableItemFactory() + payment_string = str(payment) + self.assertIn(str(payment.person), payment_string) + self.assertIn(str(payment.show_amount()), payment_string) + self.assertIn(str(payment.get_item()), payment_string) diff --git a/members/urls.py b/members/urls.py index 15f6167c..40c81774 100644 --- a/members/urls.py +++ b/members/urls.py @@ -8,6 +8,7 @@ EntryPage, userCreated, ConfirmFamily, + MembershipView, QuickpayCallback, ActivitySignup, DepartmentSignView, @@ -16,6 +17,8 @@ departmentView, Activities, AdminSignup, + PaymentsView, + QuickPayCallbackNew, ) from django.contrib.auth import views as auth_views from graphene_django.views import GraphQLView @@ -70,6 +73,11 @@ url(r"volunteer$", volunteerSignup, name="volunteer_signup"), url(r"user_created/$", userCreated, name="user_created"), url(r"admin_signup/$", AdminSignup, name="admin_signup"), + url(r"memberships/$", MembershipView, name="memberships"), + url(r"payments/$", PaymentsView, name="payments"), + url( + r"payments/quickpaycallback$", QuickPayCallbackNew, name="quickpay_callback_new" + ), url(r"family/$", FamilyDetails, name="family_detail"), url(r"family/Person/(?P[\d]+)/$", PersonUpdate, name="person_update"), url(r"family/Person/(?P[A-Z]{2})$", PersonCreate, name="person_add"), diff --git a/members/views/__init__.py b/members/views/__init__.py index 3da9b69b..b5f0ec4a 100644 --- a/members/views/__init__.py +++ b/members/views/__init__.py @@ -1,18 +1,44 @@ -# flake8: noqa # ignored since it is being used in the files -from members.views.FamilyDetails import FamilyDetails +from members.views.Activities import Activities +from members.views.ActivitySignup import ActivitySignup +from members.views.AdminSignup import AdminSignup from members.views.ConfirmFamily import ConfirmFamily +from members.views.DeclineInvitation import DeclineInvitation +from members.views.DepartmentSignView import DepartmentSignView +from members.views.departmentView import departmentView +from members.views.EntryPage import EntryPage +from members.views.FamilyDetails import FamilyDetails +from members.views.paymentGatewayErrorView import paymentGatewayErrorView from members.views.PersonCreate import PersonCreate from members.views.PersonUpdate import PersonUpdate -from members.views.WaitingListSetSubscription import WaitingListSetSubscription -from members.views.DeclineInvitation import DeclineInvitation -from members.views.ActivitySignup import ActivitySignup +from members.views.QuickpayCallback import QuickpayCallback from members.views.UpdatePersonFromForm import UpdatePersonFromForm -from members.views.EntryPage import EntryPage from members.views.userCreated import userCreated from members.views.volunteerSignup import volunteerSignup -from members.views.QuickpayCallback import QuickpayCallback -from members.views.DepartmentSignView import DepartmentSignView -from members.views.paymentGatewayErrorView import paymentGatewayErrorView -from members.views.departmentView import departmentView -from members.views.Activities import Activities -from members.views.AdminSignup import AdminSignup +from members.views.WaitingListSetSubscription import WaitingListSetSubscription + +from .memberships_view import MembershipView +from .payments_view import PaymentsView +from .quickpay_callback_new import QuickPayCallbackNew + +__all__ = [ + Activities, + ActivitySignup, + AdminSignup, + ConfirmFamily, + DeclineInvitation, + DepartmentSignView, + departmentView, + EntryPage, + FamilyDetails, + MembershipView, + paymentGatewayErrorView, + PaymentsView, + PersonCreate, + PersonUpdate, + QuickpayCallback, + QuickPayCallbackNew, + UpdatePersonFromForm, + userCreated, + volunteerSignup, + WaitingListSetSubscription, +] diff --git a/members/views/memberships_view.py b/members/views/memberships_view.py new file mode 100644 index 00000000..0d311419 --- /dev/null +++ b/members/views/memberships_view.py @@ -0,0 +1,58 @@ +from django.contrib.auth.decorators import login_required, user_passes_test +from django.http import HttpResponseRedirect +from django.shortcuts import render +from members.forms import MembershipForm +from members.models import Membership, PayableItem, Person, Union +from members.utils.user import has_user, user_to_person + + +@login_required +@user_passes_test(has_user, "/admin_signup/") +def MembershipView(request): + + logged_in_person = user_to_person(request.user) + family_members = Person.objects.filter(family=logged_in_person.family) + unions = Union.objects.all() + current_memberships = Membership.objects.filter(person__in=family_members).order_by( + "person", "sign_up_date", "union" + ) + if request.method == "GET": + form = MembershipForm(family_members) + return render( + request, + "members/memberships.html", + { + # TODO check for closed unions: + "unions": unions, + "family_members": family_members, + "current_memberships": current_memberships, + "form": form, + }, + ) + elif request.method == "POST": + form = MembershipForm(family_members, request.POST) + if form.is_valid(): + membership = Membership.objects.create( + person=form.cleaned_data["person"], union=form.cleaned_data["union"] + ) + payment = PayableItem.objects.create( + person=logged_in_person, + amount_ore=membership.union.membership_price_ore, + membership=membership, + ) + base_url = "/".join(request.build_absolute_uri().split("/")[:3]) + return HttpResponseRedirect( + payment.get_link(base_url=base_url, continue_page="payments") + ) + else: + return render( + request, + "members/memberships.html", + { + # TODO check for closed unions: + "unions": unions, + "family_members": family_members, + "current_memberships": current_memberships, + "form": form, + }, + ) diff --git a/members/views/payments_view.py b/members/views/payments_view.py new file mode 100644 index 00000000..e97b59e0 --- /dev/null +++ b/members/views/payments_view.py @@ -0,0 +1,13 @@ +from django.contrib.auth.decorators import login_required, user_passes_test +from django.shortcuts import render +from members.models import PayableItem, Person +from members.utils.user import has_user, user_to_person + + +@login_required +@user_passes_test(has_user, "/admin_signup/") +def PaymentsView(request): + logged_in_person = user_to_person(request.user) + family_members = Person.objects.filter(family=logged_in_person.family) + payments = PayableItem.objects.filter(person__in=family_members).order_by("-added") + return render(request, "members/payments.html", {"payments": payments},) diff --git a/members/views/quickpay_callback_new.py b/members/views/quickpay_callback_new.py new file mode 100644 index 00000000..ec291fc7 --- /dev/null +++ b/members/views/quickpay_callback_new.py @@ -0,0 +1,9 @@ +from django.http import HttpResponse +from django.views.decorators.csrf import csrf_exempt +from members.models import PayableItem + + +@csrf_exempt +def QuickPayCallbackNew(request): + PayableItem.send_all_payment_confirmations() + return HttpResponse("OK")