Source code for members.services
"""Services defined in the members package"""
from datetime import date
from typing import Callable, List, Dict, Any
from django.db.models import Q, Count
from django.utils import timezone
from django.utils.translation import gettext
from members import emails
from members.models import Membership, Member
from utils.snippets import datetime_to_lectureyear
def _member_group_memberships(
member: Member, condition: Callable[[Membership], bool]
) -> Dict[str, Any]:
"""
Determines the group membership of a user based on a condition
:return: Object with group memberships
"""
memberships = member.membergroupmembership_set.all()
data = {}
for membership in memberships:
if not condition(membership):
continue
period = {
"since": membership.since,
"until": membership.until,
"chair": membership.chair,
}
if hasattr(membership.group, "board"):
period["role"] = membership.role
if membership.until is None and hasattr(membership.group, "board"):
period["until"] = membership.group.board.until
name = membership.group.name
if data.get(name):
data[name]["periods"].append(period)
if data[name]["earliest"] > membership.since:
data[name]["earliest"] = membership.since
data[name]["periods"].sort(key=lambda x: x["since"])
else:
data[name] = {
"name": name,
"periods": [period],
"url": membership.group.get_absolute_url(),
"earliest": membership.since,
}
return data
[docs]def member_achievements(member) -> List:
"""
Derives a list of achievements of a member
Committee and board memberships + mentorships
"""
achievements = _member_group_memberships(
member,
lambda membership: (
hasattr(membership.group, "board") or hasattr(membership.group, "committee")
),
)
mentor_years = member.mentorship_set.all()
for mentor_year in mentor_years:
name = "Mentor in {}".format(mentor_year.year)
# Ensure mentorships appear last but are sorted
earliest = date.today()
earliest = earliest.replace(year=earliest.year + mentor_year.year)
if not achievements.get(name):
achievements[name] = {
"name": name,
"earliest": earliest,
}
return sorted(achievements.values(), key=lambda x: x["earliest"])
[docs]def member_societies(member) -> List:
"""
Derives a list of societies a member was part of
"""
societies = _member_group_memberships(
member, lambda membership: (hasattr(membership.group, "society"))
)
return sorted(societies.values(), key=lambda x: x["earliest"])
[docs]def gen_stats_member_type() -> Dict[str, int]:
"""
Generate a dictionary where every key is a member type with
the value being the number of current members of that type
"""
data = {}
for key, display in Membership.MEMBERSHIP_TYPES:
data[str(display)] = (
Membership.objects.filter(since__lte=date.today())
.filter(Q(until__isnull=True) | Q(until__gt=date.today()))
.filter(type=key)
.count()
)
return data
[docs]def gen_stats_year() -> Dict[str, Dict[str, int]]:
"""
Generate list with 6 entries, where each entry represents the total amount
of Thalia members in a year. The sixth element contains all the multi-year
students.
"""
stats_year = {}
current_year = datetime_to_lectureyear(date.today())
for i in range(5):
new = {}
for key, _ in Membership.MEMBERSHIP_TYPES:
new[key] = (
Membership.objects.filter(user__profile__starting_year=current_year - i)
.filter(since__lte=date.today())
.filter(Q(until__isnull=True) | Q(until__gt=date.today()))
.filter(type=key)
.count()
)
stats_year[str(current_year - i)] = new
# Add multi year members
new = {}
for key, _ in Membership.MEMBERSHIP_TYPES:
new[key] = (
Membership.objects.filter(user__profile__starting_year__lt=current_year - 4)
.filter(since__lte=date.today())
.filter(Q(until__isnull=True) | Q(until__gt=date.today()))
.filter(type=key)
.count()
)
stats_year[str(gettext("Older"))] = new
return stats_year
[docs]def verify_email_change(change_request) -> None:
"""
Mark the email change request as verified
:param change_request: the email change request
"""
change_request.verified = True
change_request.save()
process_email_change(change_request)
[docs]def confirm_email_change(change_request) -> None:
"""
Mark the email change request as verified
:param change_request: the email change request
"""
change_request.confirmed = True
change_request.save()
process_email_change(change_request)
[docs]def process_email_change(change_request) -> None:
"""
Change the user's email address if the request was completed and
send the completion email
:param change_request: the email change request
"""
if not change_request.completed:
return
member = change_request.member
member.email = change_request.email
member.save()
emails.send_email_change_completion_message(change_request)
[docs]def execute_data_minimisation(dry_run=False, members=None) -> List[Member]:
"""
Clean the profiles of members/users of whom the last membership ended
at least 31 days ago
:param dry_run: does not really remove data if True
:param members: queryset of members to process, optional
:return: list of processed members
"""
if not members:
members = Member.objects
members = (
members.annotate(membership_count=Count("membership"))
.exclude(
(
Q(membership__until__isnull=True)
| Q(membership__until__gt=timezone.now().date())
)
& Q(membership_count__gt=0)
)
.distinct()
.prefetch_related("membership_set", "profile")
)
deletion_period = timezone.now().date() - timezone.timedelta(days=31)
processed_members = []
for member in members:
if (
member.latest_membership is None
or member.latest_membership.until <= deletion_period
):
processed_members.append(member)
profile = member.profile
profile.student_number = None
profile.phone_number = None
profile.address_street = None
profile.address_street2 = None
profile.address_postal_code = None
profile.address_city = None
profile.address_country = None
profile.birthday = None
profile.emergency_contact_phone_number = None
profile.emergency_contact = None
member.bank_accounts.all().delete()
if not dry_run:
profile.save()
return processed_members