Coverage for gui/views.py: 100%
188 statements
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-14 06:24 +0000
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-14 06:24 +0000
1from allauth.account.decorators import verified_email_required
2from django.shortcuts import render, get_object_or_404
3from django.http import HttpResponseRedirect, Http404
4from django.urls import reverse
5from django.contrib import messages
6from django.contrib.auth import get_user_model
7from django.contrib.auth.mixins import UserPassesTestMixin
8from django.utils.safestring import mark_safe
9from django.utils.decorators import method_decorator
10from django.utils import timezone
11from django.template.loader import render_to_string
12from django.views import View
13from django.views.generic import DetailView
14from django.views.generic.edit import CreateView, FormView
15from django.views.generic.list import ListView
17from .email import AdminEmail, email_context
18from .models import Link, MembershipPeriod, Membership, Ballot
19from .forms import ProfileForm, MembershipApplicationForm, BallotForm
22class RequiresValidMembershipMixin(UserPassesTestMixin):
23 def test_func(self):
24 try:
25 membership = self.request.user.profile.membership
26 return membership is not None and membership.is_approved
27 except AttributeError:
28 # We have an anonymous user, don't give access!
29 return False
32def index(request):
33 context = {
34 "links": Link.objects.all().order_by("index_position"),
35 }
36 return render(request, 'gui/index.html', context)
39@verified_email_required
40def profile(request):
41 user = request.user
43 if request.method == 'POST':
44 form = ProfileForm(request.POST)
46 if form.is_valid():
47 form.save(user)
48 messages.add_message(request, messages.SUCCESS, 'Your profile was updated successfully')
49 return HttpResponseRedirect(reverse('index'))
50 else:
51 messages.add_message(request, messages.ERROR, mark_safe('Your profile is invalid: {}'.format(form.errors)))
52 return HttpResponseRedirect(reverse('account-profile'))
53 else:
54 form = ProfileForm(initial={"first_name": user.first_name,
55 "last_name": user.last_name,
56 "affiliation": user.profile.affiliation,
57 "public_statement": user.profile.public_statement})
59 return render(request, 'gui/profile.html', {'form': form})
62@verified_email_required
63def account_delete(request):
64 user = request.user
66 if request.method == 'POST':
67 user.delete()
68 messages.add_message(request, messages.SUCCESS, 'Your account has been deleted successfully')
69 return HttpResponseRedirect(reverse('index'))
70 else:
71 return render(request, 'gui/account_delete.html', {})
74@verified_email_required
75def membership_application(request):
76 user = request.user
78 cur_period = MembershipPeriod.current_period()
79 if cur_period is None:
80 msg = ("No membership period has been created. If you are an administrator, please create a "
81 "period to allow members to apply.")
82 return render(request, 'gui/message.html', {"title": "Membership Application",
83 "severity": "warning",
84 "panel_title": "Can't apply when no membership period exists",
85 "message": msg})
87 # Do not allow re-applying if an application exists
88 if user.profile.membership is not None:
89 msg = "You already applied to the period {}. Current status: {}".format(cur_period.short_name,
90 user.profile.membership.status)
91 title = "ERROR: An application for the period {} already exists for your user".format(cur_period.short_name)
92 return render(request, 'gui/message.html', {"title": "Membership Application",
93 "severity": "danger",
94 "panel_title": title,
95 "message": msg})
97 if request.method == 'POST':
98 form = MembershipApplicationForm(request.POST)
100 if form.is_valid() and form.cleaned_data.get('user_id') == user.id:
101 membership = form.save()
103 context = email_context(new_membership=membership)
104 AdminEmail(render_to_string('gui/email/new_membership_application_subject.txt',
105 context),
106 render_to_string('gui/email/new_membership_application_message.txt',
107 context)).send()
109 messages.add_message(request, messages.SUCCESS,
110 'Your application was sent successfully and will be reviewed shortly')
112 return HttpResponseRedirect(reverse('index'))
113 messages.add_message(request, messages.ERROR, mark_safe('Your application is invalid'))
114 return HttpResponseRedirect(reverse('membership-application'))
115 else:
116 form = MembershipApplicationForm(initial={"period_id": cur_period.id,
117 "user_id": user.id,
118 "public_statement": user.profile.public_statement,
119 "agree_membership": False})
121 return render(request, 'gui/membership_application.html', {'form': form})
124class RequiresSuperUserMixin(UserPassesTestMixin):
125 def test_func(self):
126 return self.request.user.is_superuser
129class ChangeUserStatus(RequiresSuperUserMixin, View):
130 def post(self, request, pk, superuser):
131 user = get_object_or_404(get_user_model(), pk=pk)
132 if user != self.request.user:
133 role = "administrator" if user.is_superuser else "user"
134 new_role = "administrator" if superuser else "user"
136 # Make the change
137 user.is_superuser = user.is_staff = superuser
138 user.save()
140 # Send an email to the user about the status change
141 context = email_context(role=role, new_role=new_role, admin=self.request.user)
142 user.profile.send_email("Your status changed from '{}' to '{}'".format(role, new_role),
143 render_to_string('gui/email/user_role_change_message.txt',
144 context))
146 messages.add_message(request, messages.SUCCESS,
147 "The member '{}' has been made {}".format(user.profile,
148 new_role))
149 else:
150 messages.add_message(request, messages.ERROR,
151 "You cannot change your own access rights")
152 return HttpResponseRedirect(request.META.get("HTTP_REFERER", reverse('index')))
155@method_decorator(verified_email_required, name='dispatch')
156class MembershipPeriodCreateView(RequiresSuperUserMixin, CreateView):
157 model = MembershipPeriod
158 fields = ['short_name', 'description', 'agreement_url', 'start', 'end']
159 template_name = "gui/membership_period_create.html"
161 def get_success_url(self):
162 return reverse('index')
164 def get_initial(self):
165 initial = super().get_initial()
167 cur_period = MembershipPeriod.current_period()
168 if cur_period is not None:
169 initial['short_name'] = cur_period.short_name
170 initial['description'] = cur_period.description
171 initial['agreement_url'] = cur_period.agreement_url
172 initial['start'] = cur_period.start
174 return initial
177@method_decorator(verified_email_required, name='dispatch')
178class MembershipApplicationListView(RequiresSuperUserMixin, ListView):
179 model = Membership
180 paginate_by = 10
181 template_name = "gui/membership_approval.html"
183 def get_queryset(self):
184 return Membership.pending_memberships()
186 def post(self, request, *args, **kwargs):
187 params = request.POST
189 try:
190 ids = [int(id) for id in params.getlist('ids', [])]
191 memberships = Membership.objects.filter(id__in=ids)
193 if params.get("action") == "approve":
194 for membership in memberships:
195 membership.approve()
196 messages.add_message(request, messages.SUCCESS,
197 'You approved {} membership applications'.format(len(memberships)))
198 elif params.get("action") == "reject":
199 rejected = []
200 for membership in memberships:
201 reason = params.get("reject_reason_{}".format(membership.id))
202 if reason is not None and len(reason) > 0:
203 membership.reject(reason)
204 rejected.append(membership)
205 else:
206 msg = "Can't refuse {}'s application without a reason".format(membership.user_profile)
207 messages.add_message(request, messages.ERROR, msg)
209 if len(rejected) > 0:
210 messages.add_message(request, messages.SUCCESS,
211 'You rejected {} membership applications'.format(len(rejected)))
213 else:
214 messages.add_message(request, messages.ERROR,
215 "ERROR: Unsupported action...")
216 except ValueError:
217 messages.add_message(request, messages.ERROR,
218 "ERROR: One or more IDs are not integers...")
220 return HttpResponseRedirect(reverse('membership-approval'))
223class MembersListView(RequiresValidMembershipMixin, ListView):
224 model = Membership
225 template_name = "gui/members.html"
227 def get_queryset(self):
228 period = MembershipPeriod.current_period()
229 if period is not None:
230 query = period.members.order_by("user_profile__user__last_name")
231 return query.select_related('user_profile', 'user_profile__user')
232 else:
233 return [] # pragma: no cover (we cannot have a valid membership and but no period)
236def about(request):
237 return render(request, 'gui/about.html', {})
240@method_decorator(verified_email_required, name='dispatch')
241class BallotListView(RequiresSuperUserMixin, ListView):
242 model = Ballot
245class VotingView(RequiresValidMembershipMixin, FormView):
246 template_name = 'gui/vote.html'
247 form_class = BallotForm
249 def get_ballot(self):
250 ballot = get_object_or_404(Ballot, pk=self.kwargs['pk'])
252 # TODO: Use message.html to describe the issue rather than just dying out
253 if timezone.now() < ballot.opening_on:
254 raise Http404('{} is not yet open'.format(ballot))
255 elif timezone.now() > ballot.closing_on:
256 raise Http404('{} is already closed'.format(ballot))
257 elif self.request.user.profile.membership in ballot.voters.all():
258 raise Http404('{} has already voted in the ballot'.format(self.request.user.profile.membership))
259 return ballot
261 def get_form_kwargs(self):
262 kwargs = super().get_form_kwargs()
263 kwargs['ballot'] = self.get_ballot()
264 return kwargs
266 def get_success_url(self):
267 return reverse('index')
269 def get_context_data(self, **kwargs):
270 context = super().get_context_data(**kwargs)
271 context['ballot'] = self.get_ballot()
272 return context
274 def form_valid(self, form):
275 # Since the form is valid, add the results of the vote to the DB
276 # NOTICE: the membership has to be valid, thanks to RequiresValidMembershipMixin
277 form.save(self.request.user.profile.membership)
278 messages.add_message(self.request, messages.SUCCESS, "Thanks for casting a vote!")
280 return super().form_valid(form)
283@method_decorator(verified_email_required, name='dispatch')
284class BallotAdminView(RequiresSuperUserMixin, DetailView):
285 model = Ballot
286 template_name = 'gui/ballot_admin.html'
288 def get_context_data(self, **kwargs):
289 context = super().get_context_data(**kwargs)
290 context['ballot'] = context.pop('object')
291 return context
294class BallotSendReminderView(RequiresSuperUserMixin, View):
295 def post(self, request, pk):
296 ballot = get_object_or_404(Ballot, pk=pk)
297 ballot.send_reminder()
299 messages.add_message(request, messages.SUCCESS,
300 "An email has been sent to all members who have not voted yet")
301 return HttpResponseRedirect(request.META.get("HTTP_REFERER", reverse('index')))