views.py 10.6 KB
Newer Older
1
import csv
2
import json
3
from datetime import date, datetime
4
from django.contrib.auth.decorators import login_required, permission_required
5
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
6
from django.db.models import Q
7
from django.http import HttpResponse, Http404
8
from django.shortcuts import get_object_or_404, render
9
10
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
11
from django.utils.translation import gettext as _
12
13
from django.views.generic import FormView
from django.views.generic.base import TemplateResponseMixin, View
14
from rest_framework.authtoken.models import Token
15
from rest_framework.authtoken.views import ObtainAuthToken
16
from rest_framework.response import Response
17

18
import pizzas.services
19
from members import services, emails
20
from members.models import EmailChange, Membership
21
22
23
from . import models
from .forms import ProfileForm, EmailChangeForm
from .services import member_achievements
24
from .services import member_societies
25

26

27
28
29
class ObtainThaliaAuthToken(ObtainAuthToken):

    def post(self, request, *args, **kwargs):
30
31
32
33
34
35
36
37
38
        serializer = self.serializer_class(data={
            'username': request.data.get('username').lower()
            if 'username' in request.data else None,
            'password': request.data.get('password')
        }, context={'request': request})
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)
        return Response({'token': token.key})
39
40


41
def filter_users(tab, keywords, year_range):
42
43
    memberships_query = Q(until__gt=datetime.now()) | Q(until=None)
    members_query = ~Q(id=None)
44

45
    if tab and tab.isdigit():
46
        members_query &= Q(profile__starting_year=int(tab))
47
48
        memberships_query &= Q(type=Membership.MEMBER)
    elif tab == 'older':
49
        members_query &= Q(profile__starting_year__lt=year_range[-1])
50
51
        memberships_query &= Q(type=Membership.MEMBER)
    elif tab == 'former':
52
        # Filter out all current active memberships
53
54
        memberships_query &= (Q(type=Membership.MEMBER) |
                              Q(type=Membership.HONORARY))
55
        memberships = models.Membership.objects.filter(memberships_query)
56
        members_query &= ~Q(pk__in=memberships.values('user__pk'))
57
        # Members_query contains users that are not currently (honorary)member
58
59
60
    elif tab == 'benefactors':
        memberships_query &= Q(type=Membership.BENEFACTOR)
    elif tab == 'honorary':
61
        memberships_query = Q(until__gt=datetime.now().date()) | Q(until=None)
62
        memberships_query &= Q(type=Membership.HONORARY)
63

64
65
    if keywords:
        for key in keywords:
66
            members_query &= (
67
                (Q(profile__nickname__icontains=key) &
68
                 # Works because relevant options all have `nick` in their key
69
70
71
72
                 Q(profile__display_name_preference__contains='nick')) |
                Q(first_name__icontains=key) |
                Q(last_name__icontains=key) |
                Q(username__icontains=key))
73

74
75
76
    if tab == 'former':
        memberships_query = (Q(type=Membership.MEMBER) |
                             Q(type=Membership.HONORARY))
77
78
79
80
        memberships = models.Membership.objects.filter(memberships_query)
        all_memberships = models.Membership.objects.all()
        # Only keep members that were once members, or are legacy users that
        #  do not have any memberships at all
81
82
        members_query &= (Q(pk__in=memberships.values('user__pk')) |
                          ~Q(pk__in=all_memberships.values('user__pk')))
83
84
    else:
        memberships = models.Membership.objects.filter(memberships_query)
85
        members_query &= Q(pk__in=memberships.values('user__pk'))
86
    return (models.Member.objects
87
            .filter(members_query)
88
            .annotate()
89
            .order_by('first_name'))
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109


@login_required
def index(request):
    query_filter = '' if request.GET.get(
        'filter') is None else request.GET.get('filter')
    keywords = '' if request.GET.get('keywords') is None else request.GET.get(
        'keywords').split()

    page = request.GET.get('page')
    page = 1 if page is None or not page.isdigit() else int(page)

    start_year = date.today().year - 4
    # If language is English show one year less
    # since the width is smaller than needed for the translations to fit
    if request.LANGUAGE_CODE == 'en':
        start_year += 1
    year_range = list(reversed(range(start_year, date.today().year + 1)))

    members = filter_users(query_filter, keywords, year_range)
110
111
112

    paginator = Paginator(members, 24)

Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
113
114
115
116
117
118
119
120
121
    try:
        members = paginator.page(page)
    except PageNotAnInteger:
        # If page is not an integer, deliver first page.
        members = paginator.page(1)
    except EmptyPage:
        # If page is out of range (e.g. 9999), deliver last page of results.
        members = paginator.page(paginator.num_pages)

122
123
124
    page_range = range(1, paginator.num_pages + 1)
    if paginator.num_pages > 7:
        if page > 3:
125
126
127
128
            page_range_end = paginator.num_pages
            if page + 3 <= paginator.num_pages:
                page_range_end = page + 3

Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
129
            page_range = range(page - 2, page_range_end)
130
131
132
133
            while page_range.stop - page_range.start < 5:
                page_range = range(page_range.start - 1, page_range.stop)
        else:
            page_range = range(1, 6)
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
134

Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
135
136
137
    return render(request, 'members/index.html',
                  {'members': members, 'filter': query_filter,
                   'year_range': year_range, 'page_range': page_range,
138
                   'keywords': keywords})
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
139
140


141
@login_required
142
143
def profile(request, pk=None):
    if pk:
144
        member = get_object_or_404(models.Member, pk=int(pk))
145
    else:
146
        member = request.member
Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
147

148
    # Group the memberships under the committees for easier template rendering
149
    achievements = member_achievements(member)
150
    societies = member_societies(member)
151
152

    membership = member.current_membership
153
    membership_type = _("Unknown membership history")
154
155
    if membership:
        membership_type = membership.get_type_display()
156
157
158
159
160
    elif member.has_been_honorary_member():
        membership_type = _("Former honorary member")
    elif member.has_been_member():
        membership_type = _("Former member")
    elif member.latest_membership:
161
        membership_type = _("Former benefactor")
162

163
    return render(request, 'members/profile.html',
164
165
                  {
                      'achievements': achievements,
166
                      'societies': societies,
167
168
                      'member': member,
                      'membership_type': membership_type,
169
                  })
170

171

172
173
174
175
@login_required
def account(request):
    return render(request, 'members/account.html')

176

177
178
@login_required
def edit_profile(request):
179
    profile = get_object_or_404(models.Profile, user=request.user)
180
    saved = False
181
182

    if request.POST:
183
        form = ProfileForm(request.POST, request.FILES, instance=profile)
184
        if form.is_valid():
185
            saved = True
186
187
            form.save()
    else:
188
        form = ProfileForm(instance=profile)
189
190

    return render(request, 'members/edit_profile.html',
191
                  {'form': form, 'saved': saved})
192

193

194
195
196
197
198
@permission_required('auth.change_user')
def iban_export(request):
    header_fields = ['name', 'username', 'iban']
    rows = []

199
    members = models.Member.current_members.filter(
200
        profile__direct_debit_authorized=True)
201
202

    for member in members:
203
        if member.current_membership.type != 'honorary':
204
205
            rows.append({
                'name': member.get_full_name(),
206
207
                'username': member.username,
                'iban': member.profile.bank_account
208
            })
209
210
211
212
213
214
215
216
217
218
219
220
221

    response = HttpResponse(content_type='text/csv')
    writer = csv.DictWriter(response, header_fields)
    writer.writeheader()

    for row in rows:
        writer.writerow(row)

    response['Content-Disposition'] = (
        'attachment; filename="iban-export.csv"')
    return response


222
@login_required
223
def statistics(request):
224
    member_types = [t[0] for t in Membership.MEMBERSHIP_TYPES]
225
226

    # The numbers
227
    total = models.Member.current_members.count()
228
229

    context = {
Thom Wiggers's avatar
Thom Wiggers committed
230
        "total_members": total,
231
232
233
234
235
236
237
238
        "statistics": json.dumps({
            "cohort_sizes": services.gen_stats_year(member_types),
            "member_type_distribution":
                services.gen_stats_member_type(member_types),
            "total_pizza_orders": pizzas.services.gen_stats_pizza_orders(),
            "current_pizza_orders":
                pizzas.services.gen_stats_current_pizza_orders(),
        })
239
240
241
    }

    return render(request, 'members/statistics.html', context)
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302


@method_decorator(login_required, name='dispatch')
class EmailChangeFormView(FormView):
    """
    View that renders the email change form
    """
    form_class = EmailChangeForm
    template_name = 'members/email_change.html'

    def get_initial(self):
        initial = super().get_initial()
        initial['email'] = self.request.member.email
        return initial

    def post(self, request, *args, **kwargs):
        request.POST = request.POST.dict()
        request.POST['member'] = request.member.pk
        return super().post(request, *args, **kwargs)

    def form_valid(self, form):
        change_request = form.save()
        emails.send_email_change_confirmation_messages(change_request)
        return TemplateResponse(request=self.request,
                                template='members/email_change_requested.html')


@method_decorator(login_required, name='dispatch')
class EmailChangeConfirmView(View, TemplateResponseMixin):
    """
    View that renders an HTML template and confirms the old email address
    """
    template_name = 'members/email_change_confirmed.html'

    def get(self, request, *args, **kwargs):
        if not EmailChange.objects.filter(confirm_key=kwargs['key']).exists():
            raise Http404

        change_request = EmailChange.objects.get(confirm_key=kwargs['key'])

        services.confirm_email_change(change_request)

        return self.render_to_response({})


@method_decorator(login_required, name='dispatch')
class EmailChangeVerifyView(View, TemplateResponseMixin):
    """
    View that renders an HTML template and verifies the new email address
    """
    template_name = 'members/email_change_verified.html'

    def get(self, request, *args, **kwargs):
        if not EmailChange.objects.filter(verify_key=kwargs['key']).exists():
            raise Http404

        change_request = EmailChange.objects.get(verify_key=kwargs['key'])

        services.verify_email_change(change_request)

        return self.render_to_response({})