Make changes to members module

parent 0300c1af
...@@ -26,13 +26,12 @@ class ProfileInline(admin.StackedInline): ...@@ -26,13 +26,12 @@ class ProfileInline(admin.StackedInline):
'address_street2', 'address_postal_code', 'address_city', 'address_street2', 'address_postal_code', 'address_city',
'address_country', 'student_number', 'phone_number', 'address_country', 'student_number', 'phone_number',
'receive_optin', 'receive_newsletter', 'birthday', 'receive_optin', 'receive_newsletter', 'birthday',
'show_birthday', 'direct_debit_authorized', 'bank_account', 'show_birthday', 'auto_renew', 'initials',
'initials', 'nickname', 'display_name_preference', 'nickname', 'display_name_preference', 'profile_description',
'profile_description', 'website', 'photo', 'emergency_contact', 'website', 'photo', 'emergency_contact',
'emergency_contact_phone_number', 'language', 'emergency_contact_phone_number', 'language',
'event_permissions') 'event_permissions')
model = models.Profile model = models.Profile
form = forms.ProfileForm
can_delete = False can_delete = False
...@@ -101,7 +100,8 @@ class UserAdmin(BaseUserAdmin): ...@@ -101,7 +100,8 @@ class UserAdmin(BaseUserAdmin):
'is_superuser', 'is_superuser',
AgeListFilter, AgeListFilter,
'profile__event_permissions', 'profile__event_permissions',
'profile__starting_year') 'profile__starting_year',
'profile__auto_renew')
add_fieldsets = ( add_fieldsets = (
(None, { (None, {
......
...@@ -94,8 +94,7 @@ ...@@ -94,8 +94,7 @@
"display_name_preference": "full", "display_name_preference": "full",
"language": "nl", "language": "nl",
"receive_optin": true, "receive_optin": true,
"direct_debit_authorized": false, "auto_renew": false
"bank_account": ""
} }
}, },
{ {
...@@ -121,8 +120,7 @@ ...@@ -121,8 +120,7 @@
"display_name_preference": "full", "display_name_preference": "full",
"language": "nl", "language": "nl",
"receive_optin": true, "receive_optin": true,
"direct_debit_authorized": false, "auto_renew": false
"bank_account": ""
} }
}, },
{ {
...@@ -148,8 +146,7 @@ ...@@ -148,8 +146,7 @@
"display_name_preference": "full", "display_name_preference": "full",
"language": "nl", "language": "nl",
"receive_optin": true, "receive_optin": true,
"direct_debit_authorized": false, "auto_renew": false
"bank_account": ""
} }
}, },
{ {
...@@ -175,8 +172,7 @@ ...@@ -175,8 +172,7 @@
"display_name_preference": "full", "display_name_preference": "full",
"language": "nl", "language": "nl",
"receive_optin": true, "receive_optin": true,
"direct_debit_authorized": false, "auto_renew": false
"bank_account": ""
} }
}, },
{ {
......
...@@ -15,22 +15,11 @@ class ProfileForm(forms.ModelForm): ...@@ -15,22 +15,11 @@ class ProfileForm(forms.ModelForm):
'phone_number', 'emergency_contact', 'phone_number', 'emergency_contact',
'emergency_contact_phone_number', 'emergency_contact_phone_number',
'show_birthday', 'website', 'show_birthday', 'website',
'profile_description', 'nickname', 'profile_description', 'nickname', 'initials',
'display_name_preference', 'photo', 'language', 'display_name_preference', 'photo', 'language',
'receive_optin', 'receive_newsletter'] 'receive_optin', 'receive_newsletter']
model = Profile model = Profile
def clean(self):
super().clean()
errors = {}
direct_debit_authorized = self.cleaned_data\
.get('direct_debit_authorized')
bank_account = self.cleaned_data.get('bank_account')
if direct_debit_authorized and not bank_account:
errors.update({'bank_account': _('Please enter a bank account')})
raise forms.ValidationError(errors)
class UserCreationForm(BaseUserCreationForm): class UserCreationForm(BaseUserCreationForm):
# Don't forget to edit the formset in admin.py! # Don't forget to edit the formset in admin.py!
......
...@@ -45,6 +45,20 @@ class Command(BaseCommand): ...@@ -45,6 +45,20 @@ class Command(BaseCommand):
code = current_relations.pop(member.pk, None) code = current_relations.pop(member.pk, None)
profile = member.profile profile = member.profile
if member.bank_accounts.exists():
account = member.bank_accounts.last()
bank_account = {
'name': account.name,
'bic': account.bic or '',
'iban': account.iban,
}
else:
bank_account = {
'name': '',
'bic': '',
'iban': '',
}
fields = { fields = {
'website_id': member.pk, 'website_id': member.pk,
'voornaam': member.first_name, 'voornaam': member.first_name,
...@@ -57,11 +71,7 @@ class Command(BaseCommand): ...@@ -57,11 +71,7 @@ class Command(BaseCommand):
'postcode': profile.address_postal_code, 'postcode': profile.address_postal_code,
'plaats': profile.address_city, 'plaats': profile.address_city,
'land': profile.get_address_country_display(), 'land': profile.get_address_country_display(),
'bankrekeningnummer': { 'bankrekeningnummer': bank_account,
'name': f'{profile.initials} {member.last_name}',
'bic': '',
'iban': profile.bank_account,
},
} }
replace_commands.append(ApiCommand( replace_commands.append(ApiCommand(
......
from django.db import migrations
def forwards_func(apps, schema_editor):
Membership = apps.get_model('members', 'membership')
db_alias = schema_editor.connection.alias
Membership.objects.using(db_alias).filter(
type='supporter').update(type='benefactor')
def reverse_func(apps, schema_editor):
Membership = apps.get_model('members', 'membership')
db_alias = schema_editor.connection.alias
Membership.objects.using(db_alias).filter(
type='benefactor').update(type='supporter')
class Migration(migrations.Migration):
dependencies = [
('members', '0029_profile_address_country'),
]
operations = [
migrations.RunPython(forwards_func, reverse_func),
]
from django.db import migrations
from django.utils import timezone
def forwards_func(apps, schema_editor):
Member = apps.get_model('members', 'profile')
BankAccount = apps.get_model('payments', 'BankAccount')
db_alias = schema_editor.connection.alias
for profile in Member.objects.using(db_alias).exclude(
bank_account=None).all():
BankAccount.objects.using(db_alias).create(
owner=profile.user,
initials=profile.initials or profile.user.first_name[0],
last_name=profile.user.last_name,
valid_from=timezone.now(),
mandate_no=f'{profile.user.pk}-1',
signature="data:image/png;base64,",
iban=profile.bank_account
)
def reverse_func(apps, schema_editor):
BankAccount = apps.get_model('payments', 'BankAccount')
db_alias = schema_editor.connection.alias
for account in BankAccount.objects.using(db_alias).all():
account.owner.profile.bank_account = account.iban
account.owner.profile.save()
account.delete()
class Migration(migrations.Migration):
dependencies = [
('members', '0031_benefactor_model_value'),
('payments', '0004_bankaccount'),
]
operations = [
migrations.RunPython(forwards_func, reverse_func),
]
...@@ -15,8 +15,6 @@ from django.db.models import Q ...@@ -15,8 +15,6 @@ from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import pgettext_lazy, gettext_lazy as _ from django.utils.translation import pgettext_lazy, gettext_lazy as _
from localflavor.generic.countries.sepa import IBAN_SEPA_COUNTRIES
from localflavor.generic.models import IBANField
from activemembers.models import MemberGroup, MemberGroupMembership from activemembers.models import MemberGroup, MemberGroupMembership
from utils import countries from utils import countries
...@@ -404,27 +402,15 @@ class Profile(models.Model): ...@@ -404,27 +402,15 @@ class Profile(models.Model):
default=True, default=True,
) )
# --- Direct debit information ---- # --- Membership preference ----
direct_debit_authorized = models.BooleanField( auto_renew = models.BooleanField(
choices=((True, _('Yes, I want Thalia to take the membership fees ' choices=((True, _('Yes, enable auto renewal.')),
'from my bank account through direct debit for ' (False, _('No, manual renewal required.'))),
'each year.')), verbose_name=_('Automatically renew membership'),
(False, _('No, I will pay the contribution myself'))),
verbose_name=_('Direct debit'),
help_text=_('Each year, have Thalia take the membership fees from my '
'bank account'),
default=False, default=False,
) )
bank_account = IBANField(
verbose_name=_('Bank account'),
help_text=_('Bank account for direct debit'),
include_countries=IBAN_SEPA_COUNTRIES,
blank=True,
null=True,
)
def display_name(self): def display_name(self):
pref = self.display_name_preference pref = self.display_name_preference
if pref == 'nickname' and self.nickname is not None: if pref == 'nickname' and self.nickname is not None:
......
...@@ -199,7 +199,7 @@ def execute_data_minimisation(dry_run=False, members=None): ...@@ -199,7 +199,7 @@ def execute_data_minimisation(dry_run=False, members=None):
profile.emergency_contact_phone_number = None profile.emergency_contact_phone_number = None
profile.emergency_contact = None profile.emergency_contact = None
profile.website = None profile.website = None
profile.bank_account = None member.bank_accounts.all().delete()
if not dry_run: if not dry_run:
profile.save() profile.save()
......
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "members"|capfirst %} — {{ block.super }}{% endblock %}
{% block opengraph_title %}{% trans "members"|capfirst %} — {{ block.super }}{% endblock %}
{% block body %}
<section class="page-section">
<div class="container">
<h1 class="text-center section-title">{% trans 'Account' %}</h1>
<div class="row">
<div class="col-lg-6 offset-lg-3">
<p class="text-center">
{% blocktrans trimmed with user=request.member.username %}
You’re currently logged in as <strong>{{ user }}</strong>
{% endblocktrans %}.
</p>
<hr>
<div>
<a href="{% url 'members:profile' request.member.pk %}">{% trans "show public profile"|capfirst %}</a>
<p>{% blocktrans %}Take a look at your own profile.{% endblocktrans %}</p>
</div>
<hr>
<div>
<a href="{% url 'registrations:renew' %}">{% trans "manage membership"|capfirst %}</a>
<p>{% blocktrans %}Get information about your membership or renew it.{% endblocktrans %}</p>
</div>
<hr>
<div>
<a href="{% url 'members:edit-profile' %}">{% trans "edit profile"|capfirst %}</a>
<p>{% blocktrans %}Edit your profile and avatar.{% endblocktrans %}</p>
</div>
<hr>
<div>
<a href="{% url 'password_change' %}">{% trans "change password"|capfirst %}</a>
<p>{% blocktrans %}Change your accounts' password.{% endblocktrans %}</p>
</div>
<hr>
<div>
<a href="{% url 'logout' %}">{% trans "logout"|capfirst %}</a>
<p>{% blocktrans %}Leave the restricted area of the website.{% endblocktrans %}</p>
</div>
</div>
</div>
</div>
</section>
{% endblock %}}
from django.conf.urls import url from django.urls import path, include
from django.urls import path
from . import views from . import views
app_name = "members" app_name = "members"
urlpatterns = [ urlpatterns = [
url('^profile/(?P<pk>[0-9]*)$', views.profile, name='profile'), path('iban-export/', views.iban_export,
url('^profile/edit/$', views.edit_profile, name='edit-profile'), name='iban-export'),
url('^members/iban-export/$', views.iban_export, name='iban-export'), path('members/', include([
path('profile/change-email/', views.EmailChangeFormView.as_view(), path('', views.index,
name='email-change'), name='index'),
path('profile/change-email/verify/<uuid:key>/', path('statistics/', views.statistics,
views.EmailChangeVerifyView.as_view(), name='email-change-verify'), name='statistics'),
path('profile/change-email/confirm/<uuid:key>/', path('profile/<int:pk>', views.profile,
views.EmailChangeConfirmView.as_view(), name='email-change-confirm'), name='profile'),
url('^$', views.index, name='index'), ])),
path('user/', include([
path('', views.user,
name='user'),
path('edit-profile/', views.edit_profile,
name='edit-profile'),
path('change-email/', views.EmailChangeFormView.as_view(),
name='email-change'),
path('change-email/verify/<uuid:key>/',
views.EmailChangeVerifyView.as_view(),
name='email-change-verify'),
path('change-email/confirm/<uuid:key>/',
views.EmailChangeConfirmView.as_view(),
name='email-change-confirm'),
])),
] ]
...@@ -168,8 +168,8 @@ def profile(request, pk=None): ...@@ -168,8 +168,8 @@ def profile(request, pk=None):
@login_required @login_required
def account(request): def user(request):
return render(request, 'members/account.html') return render(request, 'members/user.html')
@login_required @login_required
...@@ -191,18 +191,21 @@ def edit_profile(request): ...@@ -191,18 +191,21 @@ def edit_profile(request):
@permission_required('auth.change_user') @permission_required('auth.change_user')
def iban_export(request): def iban_export(request):
header_fields = ['name', 'username', 'iban'] header_fields = ['name', 'username', 'iban', 'bic']
rows = [] rows = []
members = models.Member.current_members.filter( members = models.Member.current_members.filter(
profile__direct_debit_authorized=True) profile__auto_renew=True)
for member in members: for member in members:
if member.current_membership.type != 'honorary': if (member.current_membership.type != 'honorary' and
member.bank_accounts.exists()):
bank_account = member.bank_accounts.last()
rows.append({ rows.append({
'name': member.get_full_name(), 'name': bank_account.name,
'username': member.username, 'username': member.username,
'iban': member.profile.bank_account 'iban': bank_account.iban,
'bic': bank_account.bic
}) })
response = HttpResponse(content_type='text/csv') response = HttpResponse(content_type='text/csv')
......
...@@ -214,17 +214,11 @@ class BankAccount(models.Model): ...@@ -214,17 +214,11 @@ class BankAccount(models.Model):
@property @property
def valid(self): def valid(self):
if self.valid_from and self.valid_until: if self.valid_from and self.valid_until:
return self.valid_from < timezone.now().date() < self.valid_until return self.valid_from <= timezone.now().date() < self.valid_until
return self.valid_from and self.valid_from < timezone.now().date() return self.valid_from and self.valid_from <= timezone.now().date()
def __str__(self): def __str__(self):
return f'{self.iban} - {self.owner.get_full_name()}' return f'{self.iban} - {self.owner.get_full_name()}'
class Meta: class Meta:
ordering = ('created_at',) ordering = ('created_at',)
...@@ -47,10 +47,11 @@ class BankAccountCreateView(SuccessMessageMixin, CreateView): ...@@ -47,10 +47,11 @@ class BankAccountCreateView(SuccessMessageMixin, CreateView):
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
BankAccount.objects.filter(mandate_no=None).delete() BankAccount.objects.filter(
BankAccount.objects.exclude(mandate_no=None).update( owner=self.request.member, mandate_no=None).delete()
valid_until=timezone.now() BankAccount.objects.filter(
) owner=self.request.member
).exclude(mandate_no=None).update(valid_until=timezone.now())
return super().form_valid(form) return super().form_valid(form)
...@@ -63,7 +64,7 @@ class BankAccountRevokeView(SuccessMessageMixin, UpdateView): ...@@ -63,7 +64,7 @@ class BankAccountRevokeView(SuccessMessageMixin, UpdateView):
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(owner=self.request.member) return super().get_queryset().filter(owner=self.request.member)
def get(self, **kwargs): def get(self, **kwargs):
raise Http404 raise Http404
......
"""Widgets provided by the payments package""" """Widgets provided by the payments package"""
import base64
from django.forms import Widget from django.forms import Widget
......
...@@ -36,7 +36,7 @@ MAIN_MENU = [ ...@@ -36,7 +36,7 @@ MAIN_MENU = [
'submenu': [ 'submenu': [
{'title': _('Member list'), 'name': 'members:index'}, {'title': _('Member list'), 'name': 'members:index'},
{'title': _('Photos'), 'name': 'photos:index'}, {'title': _('Photos'), 'name': 'photos:index'},
{'title': _('Statistics'), 'name': 'statistics'}, {'title': _('Statistics'), 'name': 'members:statistics'},
{'title': _('Styleguide'), 'name': 'styleguide'}, {'title': _('Styleguide'), 'name': 'styleguide'},
{'title': _('Become Active'), 'name': 'become-active'}, {'title': _('Become Active'), 'name': 'become-active'},
{'title': _('Nextcloud'), 'url': 'https://cloud.thalia.nu/', {'title': _('Nextcloud'), 'url': 'https://cloud.thalia.nu/',
......
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
<a href="{% url 'login' %}" class="btn btn-primary"><i <a href="{% url 'login' %}" class="btn btn-primary"><i
class="fas fa-user"></i></a> class="fas fa-user"></i></a>
{% else %} {% else %}
<a href="{% url 'account' %}" class="btn btn-primary"><i <a href="{% url 'members:user' %}" class="btn btn-primary"><i
class="fas fa-user"></i></a> class="fas fa-user"></i></a>
<button type="button" <button type="button"
class="btn btn-primary dropdown-toggle dropdown-toggle-split" class="btn btn-primary dropdown-toggle dropdown-toggle-split"
......
...@@ -36,11 +36,9 @@ from django.contrib import admin ...@@ -36,11 +36,9 @@ from django.contrib import admin
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from django.contrib.sitemaps.views import sitemap from django.contrib.sitemaps.views import sitemap
from django.urls import path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.views.i18n import JavaScriptCatalog from django.views.i18n import JavaScriptCatalog
import members
from activemembers.sitemaps import sitemap as activemembers_sitemap from activemembers.sitemaps import sitemap as activemembers_sitemap
from documents.sitemaps import sitemap as documents_sitemap from documents.sitemaps import sitemap as documents_sitemap
from events.sitemaps import sitemap as events_sitemap from events.sitemaps import sitemap as events_sitemap
...@@ -74,12 +72,7 @@ urlpatterns = [ # pylint: disable=invalid-name ...@@ -74,12 +72,7 @@ urlpatterns = [ # pylint: disable=invalid-name
url(r'^event-registration-terms/', TemplateView.as_view(template_name='singlepages/event_registration_terms.html'), name='event-registration-terms'), url(r'^event-registration-terms/', TemplateView.as_view(template_name='singlepages/event_registration_terms.html'), name='event-registration-terms'),
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^alumni/$', AlumniEventsView.as_view(), name='alumni'), url(r'^alumni/$', AlumniEventsView.as_view(), name='alumni'),
url(r'^members/', include('members.urls')),
url(r'^registration/', include('registrations.urls')), url(r'^registration/', include('registrations.urls')),
url(r'^account/', include([
url(r'^$', members.views.account, name='account'),
url(r'^finance/', include('payments.urls'))
])),
url(r'^events/', include('events.urls')), url(r'^events/', include('events.urls')),
url(r'^pizzas/', include('pizzas.urls')), url(r'^pizzas/', include('pizzas.urls')),
url(r'^newsletters/', include('newsletters.urls')), url(r'^newsletters/', include('newsletters.urls')),
...@@ -94,7 +87,6 @@ urlpatterns = [ # pylint: disable=invalid-name ...@@ -94,7 +87,6 @@ urlpatterns = [ # pylint: disable=invalid-name
url(r'^', include([ # 'for members' menu url(r'^', include([ # 'for members' menu
url(r'^become-active/', login_required(TemplateView.as_view(template_name='singlepages/become_active.html')), name='become-active'), url(r'^become-active/', login_required(TemplateView.as_view(template_name='singlepages/become_active.html')), name='become-active'),
url(r'^photos/', include('photos.urls')), url(r'^photos/', include('photos.urls')),
url(r'^statistics/$', members.views.statistics, name='statistics'),
url(r'^styleguide/$', views.styleguide, name='styleguide'), url(r'^styleguide/$', views.styleguide, name='styleguide'),
url(r'^styleguide/file/(?P<filename>[\w\-_\.]+)$', views.styleguide_file, name='styleguide-file'), url(r'^styleguide/file/(?P<filename>[\w\-_\.]+)$', views.styleguide_file, name='styleguide-file'),
])), ])),
...@@ -133,6 +125,8 @@ urlpatterns = [ # pylint: disable=invalid-name ...@@ -133,6 +125,8 @@ urlpatterns = [ # pylint: disable=invalid-name
url(r'crash/$', views.crash), url(r'crash/$', views.crash),
# Custom media paths # Custom media paths
url(r'^media/generate-thumbnail/(?P<request_path>.*)', generate_thumbnail, name='generate-thumbnail'), url(r'^media/generate-thumbnail/(?P<request_path>.*)', generate_thumbnail, name='generate-thumbnail'),
url(r'^media/private/(?P<request_path>.*)$', private_media, name='private-media') url(r'^media/private/(?P<request_path>.*)$', private_media, name='private-media'),