Coverage for website/activemembers/admin.py: 57.60%

109 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2025-08-14 10:31 +0000

1import csv 

2import datetime 

3 

4from django import forms 

5from django.contrib import admin, messages 

6from django.db.models import Q 

7from django.http import HttpResponse 

8from django.utils import timezone 

9from django.utils.translation import gettext_lazy as _ 

10 

11from activemembers import models 

12from activemembers.forms import MemberGroupForm, MemberGroupMembershipForm 

13from utils.snippets import datetime_to_lectureyear 

14 

15 

16class MemberGroupMembershipInlineFormSet(forms.BaseInlineFormSet): 

17 """Here for performance reasons, and to filter out old memberships. 

18 

19 Needed because the `__str__()` of `MemberGroupMembership` (which is 

20 displayed above each inline form) uses the username, name of the member 

21 and name of the group. 

22 """ 

23 

24 def __init__(self, *args, **kwargs): 

25 """Initialize and set queryset.""" 

26 super().__init__(*args, **kwargs) 

27 now = timezone.now() 

28 self.queryset = self.queryset.select_related("member", "group") 

29 

30 # Show only memberships that are active now. 

31 if not isinstance(self.instance, models.Board): 

32 self.queryset = self.queryset.filter( 

33 Q(until=None) | (Q(since__lte=now, until__gte=now)) 

34 ) 

35 

36 

37class MemberGroupMembershipInline(admin.StackedInline): 

38 """Inline for group memberships.""" 

39 

40 model = models.MemberGroupMembership 

41 formset = MemberGroupMembershipInlineFormSet 

42 can_delete = False 

43 ordering = ("since",) 

44 extra = 0 

45 autocomplete_fields = ("member",) 

46 

47 

48class MemberGroupAdmin(admin.ModelAdmin): 

49 """Manage the member groups.""" 

50 

51 inlines = (MemberGroupMembershipInline,) 

52 form = MemberGroupForm 

53 list_display = ("name", "since", "until", "active", "contact_address") 

54 list_filter = ( 

55 "until", 

56 "active", 

57 ) 

58 search_fields = ("name", "description") 

59 filter_horizontal = ("permissions", "chair_permissions") 

60 

61 fields = ( 

62 "name", 

63 "description", 

64 "photo", 

65 "permissions", 

66 "chair_permissions", 

67 "since", 

68 "until", 

69 "contact_mailinglist", 

70 "contact_email", 

71 "active", 

72 "display_members", 

73 ) 

74 

75 

76@admin.register(models.Committee) 

77class CommitteeAdmin(MemberGroupAdmin): 

78 """Manage the committees.""" 

79 

80 

81@admin.register(models.Society) 

82class SocietyAdmin(MemberGroupAdmin): 

83 """Manage the societies.""" 

84 

85 

86@admin.register(models.Board) 

87class BoardAdmin(admin.ModelAdmin): 

88 """Manage the board.""" 

89 

90 inlines = (MemberGroupMembershipInline,) 

91 form = MemberGroupForm 

92 exclude = ("is_board",) 

93 filter_horizontal = ("permissions",) 

94 

95 fields = ( 

96 "name", 

97 "description", 

98 "photo", 

99 "permissions", 

100 "contact_mailinglist", 

101 "contact_email", 

102 "since", 

103 "until", 

104 "display_members", 

105 ) 

106 

107 

108class TypeFilter(admin.SimpleListFilter): 

109 """Filter memberships on board-only.""" 

110 

111 title = _("group memberships") 

112 parameter_name = "group_type" 

113 

114 def lookups(self, request, model_admin): 

115 return [ 

116 ("boards", _("Only boards")), 

117 ("committees", _("Only committees")), 

118 ("societies", _("Only societies")), 

119 ] 

120 

121 def queryset(self, request, queryset): 

122 if self.value() == "boards": 

123 return queryset.exclude(group__board=None) 

124 if self.value() == "committees": 

125 return queryset.exclude(group__committee=None) 

126 if self.value() == "societies": 

127 return queryset.exclude(group__society=None) 

128 return queryset 

129 

130 

131class LectureYearFilter(admin.SimpleListFilter): 

132 """Filter the memberships on those started or ended in a lecture year.""" 

133 

134 title = _("lecture year") 

135 parameter_name = "lecture_year" 

136 

137 def lookups(self, request, model_admin): 

138 current_year = datetime_to_lectureyear(timezone.now()) 

139 first_year = datetime_to_lectureyear( 

140 models.MemberGroupMembership.objects.earliest("since").since 

141 ) 

142 

143 return [ 

144 (year, f"{year}-{year + 1}") for year in range(first_year, current_year + 1) 

145 ] 

146 

147 def queryset(self, request, queryset): 

148 if not self.value(): 

149 return queryset 

150 

151 year = int(self.value()) 

152 first_of_september = datetime.date(year=year, month=9, day=1) 

153 

154 return queryset.exclude(until__lt=first_of_september) 

155 

156 

157class ActiveMembershipsFilter(admin.SimpleListFilter): 

158 """Filter the memberships by whether they are active or not.""" 

159 

160 title = _("active memberships") 

161 parameter_name = "active" 

162 

163 def lookups(self, request, model_admin): 

164 return ( 

165 ("active", _("Active")), 

166 ("inactive", _("Inactive")), 

167 ) 

168 

169 def queryset(self, request, queryset): 

170 now = timezone.now() 

171 

172 if self.value() == "active": 

173 return queryset.filter(Q(until__isnull=True) | Q(until__gte=now)) 

174 if self.value() == "inactive": 

175 return queryset.filter(until__lt=now) 

176 return queryset 

177 

178 

179@admin.register(models.MemberGroupMembership) 

180class MemberGroupMembershipAdmin(admin.ModelAdmin): 

181 """Manage the group memberships.""" 

182 

183 form = MemberGroupMembershipForm 

184 list_display = ( 

185 "member", 

186 "group", 

187 "since", 

188 "until", 

189 "chair", 

190 "has_chair_permissions", 

191 "role", 

192 ) 

193 list_filter = ("group", TypeFilter, LectureYearFilter, ActiveMembershipsFilter) 

194 list_select_related = ( 

195 "member", 

196 "group", 

197 ) 

198 search_fields = ("member__first_name", "member__last_name", "member__email") 

199 date_hierarchy = "since" 

200 actions = ("export",) 

201 

202 # Facet counts would crash for this admin. 

203 show_facets = admin.ShowFacets.NEVER 

204 

205 def changelist_view(self, request, extra_context=None): 

206 self.message_user( 

207 request, 

208 _( 

209 "Do not edit existing memberships if the " 

210 "chair of a group has changed, add a " 

211 "new membership instead." 

212 ), 

213 messages.WARNING, 

214 ) 

215 return super().changelist_view(request, extra_context) 

216 

217 def export(self, request, queryset): 

218 response = HttpResponse(content_type="text/csv") 

219 response["Content-Disposition"] = 'attachment; filename="group_memberships.csv"' 

220 writer = csv.writer(response) 

221 writer.writerow( 

222 [ 

223 _("First name"), 

224 _("Last name"), 

225 _("Email"), 

226 _("Group"), 

227 _("Member since"), 

228 _("Member until"), 

229 _("Chair of the group"), 

230 _("Role"), 

231 ] 

232 ) 

233 

234 for membership in queryset: 

235 writer.writerow( 

236 [ 

237 membership.member.first_name, 

238 membership.member.last_name, 

239 membership.member.email, 

240 membership.group, 

241 membership.since, 

242 membership.until, 

243 membership.chair, 

244 membership.role, 

245 ] 

246 ) 

247 

248 return response 

249 

250 export.short_description = _("Export selected memberships") 

251 

252 

253@admin.register(models.Mentorship) 

254class MentorshipAdmin(admin.ModelAdmin): 

255 """Manage the mentorships.""" 

256 

257 autocomplete_fields = ("member",) 

258 search_fields = ("member__first_name", "member__last_name") 

259 list_filter = ("year",)