Coverage for gui/views.py: 100%

188 statements  

« 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 

16 

17from .email import AdminEmail, email_context 

18from .models import Link, MembershipPeriod, Membership, Ballot 

19from .forms import ProfileForm, MembershipApplicationForm, BallotForm 

20 

21 

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 

30 

31 

32def index(request): 

33 context = { 

34 "links": Link.objects.all().order_by("index_position"), 

35 } 

36 return render(request, 'gui/index.html', context) 

37 

38 

39@verified_email_required 

40def profile(request): 

41 user = request.user 

42 

43 if request.method == 'POST': 

44 form = ProfileForm(request.POST) 

45 

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}) 

58 

59 return render(request, 'gui/profile.html', {'form': form}) 

60 

61 

62@verified_email_required 

63def account_delete(request): 

64 user = request.user 

65 

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', {}) 

72 

73 

74@verified_email_required 

75def membership_application(request): 

76 user = request.user 

77 

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}) 

86 

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}) 

96 

97 if request.method == 'POST': 

98 form = MembershipApplicationForm(request.POST) 

99 

100 if form.is_valid() and form.cleaned_data.get('user_id') == user.id: 

101 membership = form.save() 

102 

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() 

108 

109 messages.add_message(request, messages.SUCCESS, 

110 'Your application was sent successfully and will be reviewed shortly') 

111 

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}) 

120 

121 return render(request, 'gui/membership_application.html', {'form': form}) 

122 

123 

124class RequiresSuperUserMixin(UserPassesTestMixin): 

125 def test_func(self): 

126 return self.request.user.is_superuser 

127 

128 

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" 

135 

136 # Make the change 

137 user.is_superuser = user.is_staff = superuser 

138 user.save() 

139 

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)) 

145 

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'))) 

153 

154 

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" 

160 

161 def get_success_url(self): 

162 return reverse('index') 

163 

164 def get_initial(self): 

165 initial = super().get_initial() 

166 

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 

173 

174 return initial 

175 

176 

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" 

182 

183 def get_queryset(self): 

184 return Membership.pending_memberships() 

185 

186 def post(self, request, *args, **kwargs): 

187 params = request.POST 

188 

189 try: 

190 ids = [int(id) for id in params.getlist('ids', [])] 

191 memberships = Membership.objects.filter(id__in=ids) 

192 

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) 

208 

209 if len(rejected) > 0: 

210 messages.add_message(request, messages.SUCCESS, 

211 'You rejected {} membership applications'.format(len(rejected))) 

212 

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...") 

219 

220 return HttpResponseRedirect(reverse('membership-approval')) 

221 

222 

223class MembersListView(RequiresValidMembershipMixin, ListView): 

224 model = Membership 

225 template_name = "gui/members.html" 

226 

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) 

234 

235 

236def about(request): 

237 return render(request, 'gui/about.html', {}) 

238 

239 

240@method_decorator(verified_email_required, name='dispatch') 

241class BallotListView(RequiresSuperUserMixin, ListView): 

242 model = Ballot 

243 

244 

245class VotingView(RequiresValidMembershipMixin, FormView): 

246 template_name = 'gui/vote.html' 

247 form_class = BallotForm 

248 

249 def get_ballot(self): 

250 ballot = get_object_or_404(Ballot, pk=self.kwargs['pk']) 

251 

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 

260 

261 def get_form_kwargs(self): 

262 kwargs = super().get_form_kwargs() 

263 kwargs['ballot'] = self.get_ballot() 

264 return kwargs 

265 

266 def get_success_url(self): 

267 return reverse('index') 

268 

269 def get_context_data(self, **kwargs): 

270 context = super().get_context_data(**kwargs) 

271 context['ballot'] = self.get_ballot() 

272 return context 

273 

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!") 

279 

280 return super().form_valid(form) 

281 

282 

283@method_decorator(verified_email_required, name='dispatch') 

284class BallotAdminView(RequiresSuperUserMixin, DetailView): 

285 model = Ballot 

286 template_name = 'gui/ballot_admin.html' 

287 

288 def get_context_data(self, **kwargs): 

289 context = super().get_context_data(**kwargs) 

290 context['ballot'] = context.pop('object') 

291 return context 

292 

293 

294class BallotSendReminderView(RequiresSuperUserMixin, View): 

295 def post(self, request, pk): 

296 ballot = get_object_or_404(Ballot, pk=pk) 

297 ballot.send_reminder() 

298 

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')))