feat: add community contribution ranks

This commit is contained in:
2026-09-29 22:18:57 +03:30
parent 0f6220f72c
commit 77567178ad
46 changed files with 2206 additions and 23 deletions

View File

@@ -2,15 +2,19 @@ from __future__ import annotations
import jwt
import uuid
import re
from django.conf import settings
from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from ninja import Query, Router
from apps.events.models import Event, Registration
from apps.gamification.services import public_badges_for_user
from apps.users.api.schemas import (
AuthorizationRoleSchema,
GoogleClaimVerifySchema,
@@ -32,12 +36,13 @@ from apps.users.api.schemas import (
UserLoginSchema,
UserOtpLoginSchema,
UserProfileSchema,
PublicMemberProfileSchema,
UserRegistrationSchema,
UserUpdateSchema,
UsernameCheckSchema,
)
from apps.blog.permissions import ASSOCIATION_ADMIN_GROUP, BLOG_EDITOR_GROUP, BLOG_SUPERVISOR_GROUP
from apps.users.email_identity import normalize_email_identity
from apps.users.email_identity import is_placeholder_email, normalize_email_identity
from apps.users.models import Major, University, User
from apps.users.services.auth import (
AuthServiceError,
@@ -81,6 +86,9 @@ CURATED_ROLE_GROUPS = {
ASSOCIATION_ADMIN_GROUP,
}
PUBLIC_USERNAME_PATTERN = re.compile(r"^[A-Za-z0-9_]+$")
PUBLIC_USERNAME_MIN_LENGTH = 5
ROLE_SPECS = [
{
"key": BLOG_EDITOR_GROUP,
@@ -374,11 +382,83 @@ def get_profile(request):
return request.auth
@auth_router.get("/public-profile/{username}", response={200: PublicMemberProfileSchema, 404: ErrorSchema})
def get_public_profile(request, username: str):
user = User.objects.filter(username=username, is_active=True, is_deleted=False).select_related(
"university", "major"
).first()
if not user:
return 404, {"error": "پروفایل پیدا نشد."}
details_visible = user.show_public_profile_details
profile_picture = None
if user.profile_picture and user.profile_picture.name:
profile_picture = request.build_absolute_uri(user.profile_picture.url)
events = []
registrations = (
Registration.objects.filter(
user=user,
event__is_deleted=False,
event__status__in=[Event.StatusChoices.PUBLISHED, Event.StatusChoices.COMPLETED],
status__in=[Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.ATTENDED],
)
.select_related("event")
.order_by("-event__start_time")[:12]
)
for registration in registrations:
event = registration.event
events.append({
"title": event.title,
"slug": event.slug,
"start_time": event.start_time,
"image_url": request.build_absolute_uri(event.featured_image.url) if event.featured_image else None,
"participation": registration.status,
})
profile = {
"username": user.username,
"first_name": user.first_name,
"last_name": user.last_name,
"full_name": user.get_full_name() or user.username,
"profile_picture": profile_picture,
"bio": user.bio or "",
"details_visible": details_visible,
"mobile": user.mobile if details_visible else None,
"student_id": user.student_id if details_visible else None,
"year_of_study": user.year_of_study if details_visible else None,
"university": user.get_university_display() if details_visible else None,
"major": user.get_major_display() if details_visible else None,
"badges": public_badges_for_user(user),
"events": events,
}
if details_visible and user.email and not is_placeholder_email(user.email):
profile["email"] = user.email
return 200, profile
@auth_router.put("/profile", response={200: UserProfileSchema, 400: ErrorSchema}, auth=jwt_auth)
def update_profile(request, data: UserUpdateSchema):
user = request.auth
payload = data.dict(exclude_unset=True)
if "username" in payload:
username = (payload["username"] or "").strip()
username_field = User._meta.get_field("username")
if not username or len(username) > username_field.max_length:
return 400, {"error": "نام کاربری باید بین ۱ تا ۱۵۰ نویسه باشد."}
if username != user.username and (
len(username) < PUBLIC_USERNAME_MIN_LENGTH or not PUBLIC_USERNAME_PATTERN.fullmatch(username)
):
return 400, {"error": "نام کاربری جدید باید حداقل ۵ نویسه و فقط شامل حروف انگلیسی، عدد و زیرخط باشد."}
try:
username_field.run_validators(username)
except ValidationError:
return 400, {"error": "نام کاربری فقط می‌تواند شامل حروف، عدد و نویسه‌های مجاز باشد."}
if User.objects.filter(username__iexact=username).exclude(pk=user.pk).exists():
return 400, {"error": "این نام کاربری قبلاً استفاده شده است."}
payload["username"] = username
if "major" in payload:
code = payload.pop("major")
if code:
@@ -565,7 +645,7 @@ def update_user_authorization(request, user_id: int, data: UserAuthorizationUpda
@auth_router.get("/check-username", response=UsernameCheckSchema)
def check_username_availability(request, username: str):
return {"exists": User.objects.filter(username=username).exists()}
return {"exists": User.objects.filter(username__iexact=username).exists()}
@auth_router.get("/check-mobile", response={200: MobileLookupSchema, 400: ErrorSchema})