from django.db import transaction
from django.http import JsonResponse
from users.models import User
from .models import Score
from .scoring import calculate_match_score
[docs]
def get_top_matches_for_mentee(mentee, limit=10):
"""
Return the highest-scoring :class:`~matching.models.Score` rows for a mentee.
Results are ordered by ``score`` descending and sliced to ``limit`` rows.
No filtering for blacklisted or full mentors is applied here — this is a
raw score lookup intended for administrative or diagnostic use. For the
filtered, serialized match list used in the matching UI, see
:func:`matching.services.get_ranked_matches`.
:param mentee: The mentee whose scores are being queried.
:type mentee: users.models.User
:param limit: Maximum number of score rows to return. Defaults to ``10``.
:type limit: int
:returns: Queryset of :class:`~matching.models.Score` instances ordered
by score descending, sliced to ``limit``.
:rtype: django.db.models.QuerySet
Example::
>>> get_top_matches_for_mentee(mentee_user, limit=5)
<QuerySet [<Score: mentee@g.ucla.edu - mentor@ucla.edu: 0.91>, ...]>
"""
return Score.objects.filter(
mentee_email=mentee.email
).order_by('-score')[:limit]
[docs]
def get_top_matches_for_mentor(mentor, limit=10):
"""
Return the highest-scoring :class:`~matching.models.Score` rows for a mentor.
Results are ordered by ``score`` descending and sliced to ``limit`` rows.
Intended for administrative or diagnostic use; does not apply blacklist
or capacity filtering.
:param mentor: The mentor whose scores are being queried.
:type mentor: users.models.User
:param limit: Maximum number of score rows to return. Defaults to ``10``.
:type limit: int
:returns: Queryset of :class:`~matching.models.Score` instances ordered
by score descending, sliced to ``limit``.
:rtype: django.db.models.QuerySet
Example::
>>> get_top_matches_for_mentor(mentor_user, limit=5)
<QuerySet [<Score: mentee@g.ucla.edu - mentor@ucla.edu: 0.91>, ...]>
"""
return Score.objects.filter(
mentor_email=mentor.email
).order_by('-score')[:limit]
def _has_enough_profile_data(user):
"""
Check whether a user has the minimum profile data required for score calculation.
A user is considered scoreable if they have at least one declared
``major`` and a non-zero ``year``. Users who fail this check are
excluded from both full recalculations and per-mentee refreshes to avoid
producing meaningless scores from incomplete profiles.
:param user: The user to evaluate.
:type user: users.models.User
:returns: ``True`` if ``major`` is non-empty and ``year`` is truthy,
``False`` otherwise.
:rtype: bool
.. note::
This is an internal helper; it is not intended to be called directly
from views or external modules.
Example::
>>> _has_enough_profile_data(complete_user)
True
>>> _has_enough_profile_data(incomplete_user) # major or year missing
False
"""
return bool(user.major and user.year)
[docs]
def recalculate_all_scores():
"""
Rebuild the entire :class:`~matching.models.Score` table from scratch.
Deletes all existing score rows, then computes a fresh
:func:`~matching.scoring.calculate_match_score` for every eligible
mentee–mentor pair and bulk-inserts the results in batches of 1 000
within a single database transaction.
Users on either side who do not pass :func:`_has_enough_profile_data`
are excluded from the calculation entirely.
.. warning::
This function performs a full table wipe before re-inserting. It is
intended for scheduled background jobs or admin-triggered resets, not
for request-cycle use. For incremental updates triggered by a profile
change, use :func:`recalculate_scores_for_mentee` instead.
:returns: None. Prints the number of score records created to stdout.
Example::
>>> recalculate_all_scores()
Created 240 score records
"""
Score.objects.all().delete()
mentees = [u for u in User.objects.filter(role=User.Role.MENTEE) if _has_enough_profile_data(u)]
mentors = [u for u in User.objects.filter(role=User.Role.MENTOR) if _has_enough_profile_data(u)]
scores_to_create = [
Score(
mentee_email=mentee.email,
mentor_email=mentor.email,
score=calculate_match_score(mentee, mentor),
)
for mentee in mentees
for mentor in mentors
]
with transaction.atomic():
Score.objects.bulk_create(scores_to_create, batch_size=1000)
print(f"Created {len(scores_to_create)} score records")
#: Exact confirmation string a user must submit to complete an unmatch request.
#: Referenced by :func:`require_confirmation` and surfaced in error messages
#: so the frontend can display the expected phrase to the user.
UNMATCH_CONFIRMATION = "I would like to unmatch"
[docs]
def require_auth(request):
"""
Guard helper that rejects unauthenticated requests.
Intended to be called at the top of matching views using the walrus
operator pattern::
if err := require_auth(request):
return err
:param request: The incoming HTTP request.
:type request: django.http.HttpRequest
:returns: A ``JsonResponse`` with HTTP 401 if the user is not
authenticated, or ``None`` if the check passes.
:rtype: django.http.JsonResponse or None
"""
if not request.user.is_authenticated:
return JsonResponse({'error': 'Authentication required'}, status=401)
return None
[docs]
def require_role(request, role):
"""
Guard helper that rejects requests from users who do not hold a specific role.
Intended to be called at the top of matching views using the walrus
operator pattern::
if err := require_role(request, 'MENTEE'):
return err
:param request: The incoming HTTP request.
:type request: django.http.HttpRequest
:param role: The required role string, e.g. ``'MENTEE'`` or ``'MENTOR'``.
:type role: str
:returns: A ``JsonResponse`` with HTTP 403 if the user's role does not
match, or ``None`` if the check passes.
:rtype: django.http.JsonResponse or None
"""
if request.user.role != role:
return JsonResponse(
{'error': f'Only {role.lower()}s can perform this action'},
status=403,
)
return None
[docs]
def require_confirmation(data, expected=UNMATCH_CONFIRMATION):
"""
Guard helper that validates a user-supplied confirmation string.
Prevents destructive unmatch operations from being triggered accidentally
by requiring the caller to echo back the exact phrase defined in
:data:`UNMATCH_CONFIRMATION`. The comparison is performed after stripping
leading and trailing whitespace.
Intended to be called in views using the walrus operator pattern::
if err := require_confirmation(data):
return err
:param data: Parsed JSON body of the request, expected to contain a
``'confirmation'`` key.
:type data: dict
:param expected: The exact string the ``'confirmation'`` value must match.
Defaults to :data:`UNMATCH_CONFIRMATION`.
:type expected: str
:returns: A ``JsonResponse`` with HTTP 400 and an instructional error
message if the confirmation does not match, or ``None`` if it passes.
:rtype: django.http.JsonResponse or None
"""
if data.get('confirmation', '').strip() != expected:
return JsonResponse(
{
'error': 'Confirmation failed',
'message': (
f'Please type "{expected}" to confirm. '
'After unmatching, this mentor will no longer appear in your recommendations.'
),
},
status=400,
)
return None
[docs]
def recalculate_scores_for_mentee(mentee):
"""
Incrementally refresh compatibility scores between one mentee and all eligible mentors.
Uses ``update_or_create`` to upsert a :class:`~matching.models.Score` row
for every mentor who passes :func:`_has_enough_profile_data`, preserving
scores for existing pairs while creating rows for any new mentors. If
the mentee themselves does not have enough profile data the function
returns immediately without writing anything.
Intended to be called after a mentee updates their profile so that the
match dashboard reflects their latest information without requiring a
full table rebuild. For a complete reset of all scores, use
:func:`recalculate_all_scores` instead.
:param mentee: The mentee whose scores should be refreshed.
:type mentee: users.models.User
:returns: None. Returns early without side effects if the mentee's
profile is incomplete.
Example::
>>> recalculate_scores_for_mentee(mentee_user)
"""
if not _has_enough_profile_data(mentee):
return
mentors = [u for u in User.objects.filter(role=User.Role.MENTOR) if _has_enough_profile_data(u)]
for mentor in mentors:
Score.objects.update_or_create(
mentee_email=mentee.email,
mentor_email=mentor.email,
defaults={'score': calculate_match_score(mentee, mentor)},
)
[docs]
def require_not_deleted(request):
"""
Guard helper that rejects requests from accounts pending deletion.
Prevents soft-deleted users from accessing matching features while their
account is in the grace period. Intended to be called at the top of
matching views using the walrus operator pattern::
if err := require_not_deleted(request):
return err
:param request: The incoming HTTP request.
:type request: django.http.HttpRequest
:returns: A ``JsonResponse`` with HTTP 403 and a cancellation prompt if
the account is soft-deleted, or ``None`` if the check passes.
:rtype: django.http.JsonResponse or None
"""
if request.user.is_deleted:
return JsonResponse({
'error': 'Account pending deletion',
'message': 'Cancel deletion to access this feature.'
}, status=403)
return None