initial commit
This commit is contained in:
122
apps/users/admin.py
Normal file
122
apps/users/admin.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from django import forms
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
from simplemde.widgets import SimpleMDEEditor
|
||||
|
||||
from apps.users.models import User, University, Major
|
||||
from apps.users.resources import UserResource
|
||||
from apps.users.tasks import send_verification_email
|
||||
from core.admin import SoftDeleteListFilter, BaseModelAdmin
|
||||
|
||||
|
||||
class UserAdminForm(forms.ModelForm):
|
||||
bio = forms.CharField(widget=SimpleMDEEditor(), required=False)
|
||||
student_id = forms.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin, BaseModelAdmin, ImportExportModelAdmin):
|
||||
form = UserAdminForm
|
||||
resource_class = UserResource
|
||||
list_display = ('email', 'username', 'university', 'is_email_verified', 'date_joined')
|
||||
list_filter = ('is_email_verified', 'is_staff', 'year_of_study', SoftDeleteListFilter)
|
||||
search_fields = ('email', 'username', 'student_id', 'first_name', 'last_name')
|
||||
ordering = ('-date_joined',)
|
||||
|
||||
fieldsets = (
|
||||
('Auth Credentials', {'fields': ('username', 'email', 'password')}),
|
||||
('Personal info', {
|
||||
'fields': ('first_name', 'last_name', 'student_id', 'university', 'year_of_study', 'major', 'bio', 'profile_picture')
|
||||
}),
|
||||
('Permissions', {
|
||||
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions',),
|
||||
}),
|
||||
('Important dates', {'fields': ('last_login', 'date_joined')}),
|
||||
|
||||
('Email Verification', {
|
||||
'fields': ('is_email_verified', 'email_verification_token', 'email_verification_sent_at')
|
||||
}),
|
||||
('Password Reset', {
|
||||
'fields': ('password_reset_token', 'password_reset_token_expires_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Soft Delete', {
|
||||
'fields': ('is_deleted', 'deleted_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
add_fieldsets = (
|
||||
(
|
||||
'Step 1',
|
||||
{
|
||||
'classes': ('wide',),
|
||||
'fields': ('email', 'student_id', 'password1', 'password2', 'usable_password'),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
readonly_fields = ('email_verification_token', 'email_verification_sent_at', 'deleted_at',
|
||||
'password_reset_token', 'password_reset_token_expires_at')
|
||||
|
||||
actions = BaseModelAdmin.actions + [
|
||||
'verify_emails',
|
||||
'resend_verification_email',
|
||||
]
|
||||
|
||||
@admin.action(description='Verify selected user emails')
|
||||
def verify_emails(self, request, queryset):
|
||||
queryset.update(is_email_verified=True)
|
||||
self.message_user(request, f'Verified {queryset.count()} user emails.')
|
||||
|
||||
@admin.action(description="Resend verification email")
|
||||
def resend_verification_email(self, request, queryset):
|
||||
qs = queryset.filter(is_email_verified=False).exclude(email__isnull=True).exclude(email="")
|
||||
|
||||
total = queryset.count()
|
||||
to_send = qs.count()
|
||||
skipped = total - to_send
|
||||
sent = failed = 0
|
||||
|
||||
for user in qs:
|
||||
try:
|
||||
user.regenerate_verification_token()
|
||||
user.email_verification_sent_at = timezone.now()
|
||||
user.save(update_fields=["email_verification_sent_at"])
|
||||
|
||||
verification_url = f"{settings.FRONTEND_ROOT}verify-email/{user.email_verification_token}"
|
||||
send_verification_email.delay(user.id, verification_url)
|
||||
sent += 1
|
||||
except Exception as exc:
|
||||
failed += 1
|
||||
|
||||
if sent:
|
||||
self.message_user(request, f"ایمیل تأیید برای {sent} کاربر ارسال شد.", level=messages.SUCCESS)
|
||||
if skipped:
|
||||
self.message_user(
|
||||
request,
|
||||
f"{skipped} کاربر کنار گذاشته شدند (یا قبلاً تأیید شدهاند یا ایمیل ندارند).",
|
||||
level=messages.WARNING,
|
||||
)
|
||||
if failed:
|
||||
self.message_user(request, f"ارسال برای {failed} کاربر با خطا مواجه شد.", level=messages.ERROR)
|
||||
|
||||
|
||||
@admin.register(University)
|
||||
class UniversityAdmin(BaseModelAdmin):
|
||||
list_display = ('name', 'code', 'is_active', 'created_at')
|
||||
list_filter = ('is_active', SoftDeleteListFilter)
|
||||
search_fields = ('name', 'code')
|
||||
|
||||
|
||||
@admin.register(Major)
|
||||
class MajorAdmin(BaseModelAdmin):
|
||||
list_display = ('name', 'code', 'is_active', 'created_at')
|
||||
list_filter = ('is_active', SoftDeleteListFilter)
|
||||
search_fields = ('name', 'code')
|
||||
0
apps/users/api/__init__.py
Normal file
0
apps/users/api/__init__.py
Normal file
15
apps/users/api/meta.py
Normal file
15
apps/users/api/meta.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from ninja import Router
|
||||
|
||||
from apps.users.models import Major, University
|
||||
|
||||
meta_router = Router(tags=['meta'])
|
||||
|
||||
@meta_router.get("/majors")
|
||||
def list_majors(request):
|
||||
majors = Major.objects.filter(is_deleted=False, is_active=True).order_by("name")
|
||||
return [{"id": m.id, "code": m.code, "label": m.name} for m in majors]
|
||||
|
||||
@meta_router.get("/universities")
|
||||
def list_universities(request):
|
||||
universities = University.objects.filter(is_deleted=False, is_active=True).order_by("name")
|
||||
return [{"id": u.id, "code": u.code, "label": u.name} for u in universities]
|
||||
129
apps/users/api/schemas.py
Normal file
129
apps/users/api/schemas.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Authentication-related API schemas."""
|
||||
|
||||
from ninja import Schema, ModelSchema
|
||||
from typing import Optional
|
||||
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
class UserRegistrationSchema(Schema):
|
||||
username: str
|
||||
email: str
|
||||
password: str
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
university: Optional[str] = None
|
||||
student_id: Optional[str] = None
|
||||
year_of_study: Optional[int] = None
|
||||
major: Optional[str] = None
|
||||
|
||||
class UserLoginSchema(Schema):
|
||||
email: str
|
||||
password: str
|
||||
|
||||
class UserProfileSchema(ModelSchema):
|
||||
profile_picture: Optional[str] = None
|
||||
student_id: Optional[str] = None
|
||||
major: Optional[str] = None
|
||||
university: Optional[str] = None
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
'id',
|
||||
'username',
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'student_id',
|
||||
'year_of_study',
|
||||
'major',
|
||||
'university',
|
||||
'bio',
|
||||
'date_joined',
|
||||
'is_email_verified',
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
'is_deleted',
|
||||
'deleted_at',
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def resolve_major(obj):
|
||||
return obj.get_major_display()
|
||||
|
||||
@staticmethod
|
||||
def resolve_university(obj):
|
||||
return obj.get_university_display()
|
||||
|
||||
@staticmethod
|
||||
def resolve_profile_picture(obj, context):
|
||||
"""
|
||||
Resolves the absolute URL for the profile picture.
|
||||
`context` contains the request object, which is needed for build_absolute_uri.
|
||||
"""
|
||||
request = context['request']
|
||||
if obj.profile_picture and hasattr(obj.profile_picture, 'url'):
|
||||
return request.build_absolute_uri(obj.profile_picture.url)
|
||||
return None
|
||||
|
||||
|
||||
class UserListSchema(ModelSchema):
|
||||
major: Optional[str] = None
|
||||
university: Optional[str] = None
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
'id',
|
||||
'username',
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
'date_joined',
|
||||
'major',
|
||||
'university',
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def resolve_full_name(obj):
|
||||
return obj.get_full_name()
|
||||
|
||||
@staticmethod
|
||||
def resolve_major(obj):
|
||||
return obj.get_major_display()
|
||||
|
||||
@staticmethod
|
||||
def resolve_university(obj):
|
||||
return obj.get_university_display()
|
||||
|
||||
class UserUpdateSchema(Schema):
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
bio: Optional[str] = None
|
||||
year_of_study: Optional[int] = None
|
||||
major: Optional[str] = None
|
||||
university: Optional[str] = None
|
||||
student_id: Optional[str] = None
|
||||
|
||||
class TokenSchema(Schema):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
class TokenRefreshIn(Schema):
|
||||
refresh_token: str
|
||||
|
||||
class PasswordResetRequestSchema(Schema):
|
||||
email: str
|
||||
|
||||
class PasswordResetConfirmSchema(Schema):
|
||||
token: str
|
||||
new_password: str
|
||||
|
||||
class UsernameCheckSchema(Schema):
|
||||
exists: bool
|
||||
403
apps/users/api/views.py
Normal file
403
apps/users/api/views.py
Normal file
@@ -0,0 +1,403 @@
|
||||
from typing import List
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
import uuid
|
||||
import jwt
|
||||
from ninja import Query, Router
|
||||
|
||||
from apps.users.models import User, Major, University
|
||||
from apps.users.tasks import send_verification_email, send_password_reset_email
|
||||
from apps.users.api.schemas import (
|
||||
PasswordResetConfirmSchema,
|
||||
PasswordResetRequestSchema,
|
||||
TokenRefreshIn,
|
||||
TokenSchema,
|
||||
UserListSchema,
|
||||
UserLoginSchema,
|
||||
UserProfileSchema,
|
||||
UserRegistrationSchema,
|
||||
UserUpdateSchema,
|
||||
UsernameCheckSchema,
|
||||
)
|
||||
from core.api.schemas import ErrorSchema, MessageSchema
|
||||
from core.authentication import create_jwt_token, create_refresh_token, jwt_auth
|
||||
|
||||
auth_router = Router()
|
||||
|
||||
def _get_major_from_code(code: str | None):
|
||||
if not code:
|
||||
return None
|
||||
return Major.objects.filter(code=code, is_deleted=False).first()
|
||||
|
||||
|
||||
def _get_university_from_code(code: str | None):
|
||||
if not code:
|
||||
return None
|
||||
return University.objects.filter(code=code, is_deleted=False).first()
|
||||
|
||||
|
||||
@auth_router.post("/register", response={201: MessageSchema, 400: ErrorSchema})
|
||||
def register(request, data: UserRegistrationSchema):
|
||||
"""Register a new user"""
|
||||
try:
|
||||
if data.student_id and len(str(data.student_id)) < 10:
|
||||
return 400, {"error": "Student ID must be at least 10 characters long."}
|
||||
|
||||
major_obj = None
|
||||
if data.major:
|
||||
major_obj = _get_major_from_code(data.major)
|
||||
if not major_obj:
|
||||
return 400, {"error": "Selected major is not recognized."}
|
||||
|
||||
university_obj = None
|
||||
if data.university:
|
||||
university_obj = _get_university_from_code(data.university)
|
||||
if not university_obj:
|
||||
return 400, {"error": "Selected university is not recognized."}
|
||||
|
||||
if User.objects.filter(username=data.username).exists():
|
||||
return 400, {"error": "Username is already in use."}
|
||||
|
||||
if User.objects.filter(email=data.email).exists():
|
||||
return 400, {"error": "Email is already registered."}
|
||||
|
||||
if (
|
||||
data.student_id
|
||||
and university_obj
|
||||
and User.objects.filter(
|
||||
university=university_obj, student_id=data.student_id
|
||||
).exists()
|
||||
):
|
||||
return 400, {"error": "This student ID is already registered at that university."}
|
||||
|
||||
User.objects.create_user(
|
||||
username=data.username,
|
||||
email=data.email,
|
||||
password=data.password,
|
||||
student_id=data.student_id,
|
||||
first_name=data.first_name or "",
|
||||
last_name=data.last_name or "",
|
||||
year_of_study=data.year_of_study,
|
||||
major=major_obj,
|
||||
university=university_obj,
|
||||
)
|
||||
|
||||
return 201, {"message": "Registration successful. Please check your inbox to verify your email."}
|
||||
|
||||
except Exception as e:
|
||||
return 400, {
|
||||
"error": "Unable to register user.",
|
||||
"details": str(e),
|
||||
}
|
||||
|
||||
@auth_router.post("/login", response={200: TokenSchema, 401: ErrorSchema})
|
||||
def login(request, data: UserLoginSchema):
|
||||
"""Login user and return JWT tokens"""
|
||||
user = authenticate(email=data.email, password=data.password)
|
||||
|
||||
if not user:
|
||||
return 401, {"error": "ایمیل یا رمز عبور نادرست است."}
|
||||
|
||||
if not user.is_email_verified:
|
||||
return 401, {"error": "برای ورود، ابتدا ایمیل خود را تأیید کنید."}
|
||||
|
||||
if not user.is_active:
|
||||
return 401, {"error": "حساب کاربری شما غیرفعال است."}
|
||||
|
||||
access_token = create_jwt_token(user)
|
||||
refresh_token = create_refresh_token(user)
|
||||
|
||||
return 200, {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer"
|
||||
}
|
||||
|
||||
@auth_router.post("/refresh", response={200: TokenSchema, 401: ErrorSchema})
|
||||
def refresh_tokens(request, data: TokenRefreshIn):
|
||||
"""Exchange a valid refresh token for a new access (and refresh) token."""
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
data.refresh_token,
|
||||
settings.JWT_SECRET_KEY,
|
||||
algorithms=[settings.JWT_ALGORITHM],
|
||||
)
|
||||
if payload.get("type") != "refresh":
|
||||
return 401, {"error": "نوع توکن نامعتبر است."}
|
||||
|
||||
user_id = payload.get("user_id")
|
||||
if not user_id:
|
||||
return 401, {"error": "دادههای توکن نامعتبر است."}
|
||||
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
|
||||
if not user.is_email_verified:
|
||||
return 401, {"error": "برای استفاده، ابتدا ایمیل خود را تأیید کنید."}
|
||||
|
||||
if not user.is_active:
|
||||
return 401, {"error": "حساب کاربری شما غیرفعال است."}
|
||||
|
||||
except jwt.ExpiredSignatureError:
|
||||
return 401, {"error": "رفرشتوکن منقضی شده است."}
|
||||
|
||||
except jwt.InvalidTokenError:
|
||||
return 401, {"error": "رفرشتوکن نامعتبر است."}
|
||||
|
||||
access_token = create_jwt_token(user)
|
||||
refresh_token = create_refresh_token(user)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
@auth_router.get("/verify-email/{token}", response={200: MessageSchema, 400: ErrorSchema})
|
||||
def verify_email(request, token: str):
|
||||
"""Verify user email with token"""
|
||||
try:
|
||||
user = get_object_or_404(User, email_verification_token=token)
|
||||
|
||||
if user.is_email_verified:
|
||||
return 400, {"error": "ایمیل قبلاً تأیید شده است."}
|
||||
|
||||
user.is_email_verified = True
|
||||
user.save(update_fields=['is_email_verified'])
|
||||
|
||||
return 200, {"message": "ایمیل شما با موفقیت تأیید شد."}
|
||||
|
||||
except User.DoesNotExist:
|
||||
return 400, {"error": "توکن تأیید نامعتبر است."}
|
||||
|
||||
@auth_router.post("/resend-verification", response={200: MessageSchema, 400: ErrorSchema})
|
||||
def resend_verification(request, email: str):
|
||||
"""Resend verification email"""
|
||||
try:
|
||||
user = get_object_or_404(User, email=email)
|
||||
|
||||
if user.is_email_verified:
|
||||
return 400, {"error": "ایمیل قبلاً تأیید شده است."}
|
||||
|
||||
# Generate new token
|
||||
user.regenerate_verification_token()
|
||||
user.email_verification_sent_at = timezone.now()
|
||||
user.save(update_fields=['email_verification_sent_at'])
|
||||
|
||||
# Send verification email
|
||||
verification_url = f"{settings.FRONTEND_ROOT}verify-email/{user.email_verification_token}"
|
||||
send_verification_email.delay(user.id, verification_url)
|
||||
|
||||
return 200, {"message": "ایمیل تأیید برای شما ارسال شد."}
|
||||
|
||||
except User.DoesNotExist:
|
||||
return 400, {"error": "کاربر یافت نشد."}
|
||||
|
||||
@auth_router.get("/profile", response=UserProfileSchema, auth=jwt_auth)
|
||||
def get_profile(request):
|
||||
"""Get current user profile"""
|
||||
return request.auth
|
||||
|
||||
@auth_router.put("/profile", response={200: UserProfileSchema, 400: ErrorSchema}, auth=jwt_auth)
|
||||
def update_profile(request, data: UserUpdateSchema):
|
||||
"""Update current user profile"""
|
||||
user = request.auth
|
||||
payload = data.dict(exclude_unset=True)
|
||||
|
||||
if "major" in payload:
|
||||
code = payload.pop("major")
|
||||
if code:
|
||||
major_obj = _get_major_from_code(code)
|
||||
if not major_obj:
|
||||
return 400, {"error": "UcO_ O<>OrU?UOU? O<>U^UcU+ O<>O<EFBFBD>UOUOO_."}
|
||||
payload["major"] = major_obj
|
||||
else:
|
||||
payload["major"] = None
|
||||
|
||||
if "university" in payload:
|
||||
code = payload.pop("university")
|
||||
if code:
|
||||
uni_obj = _get_university_from_code(code)
|
||||
if not uni_obj:
|
||||
return 400, {"error": "UcO U.U^OO<>O_ O<>U^UcU+ O<>O<EFBFBD>UOUOO_."}
|
||||
payload["university"] = uni_obj
|
||||
else:
|
||||
payload["university"] = None
|
||||
|
||||
for field, value in payload.items():
|
||||
setattr(user, field, value)
|
||||
|
||||
user.save()
|
||||
return 200, user
|
||||
|
||||
@auth_router.post("/profile/picture", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth)
|
||||
def upload_profile_picture(request):
|
||||
"""Upload profile picture"""
|
||||
if 'file' not in request.FILES:
|
||||
return 400, {"error": "فایلی ارسال نشده است."}
|
||||
|
||||
file = request.FILES['file']
|
||||
|
||||
# Validate file type
|
||||
if not file.content_type.startswith('image/'):
|
||||
return 400, {"error": "فایل باید از نوع تصویر باشد."}
|
||||
|
||||
# Validate file size (5MB max)
|
||||
if file.size > 5 * 1024 * 1024:
|
||||
return 400, {"error": "حجم فایل باید کمتر از ۵ مگابایت باشد."}
|
||||
|
||||
user = request.auth
|
||||
|
||||
# Delete old profile picture if exists
|
||||
if user.profile_picture:
|
||||
default_storage.delete(user.profile_picture.name)
|
||||
|
||||
# Save new profile picture
|
||||
filename = f"profile_pictures/{user.id}_{uuid.uuid4().hex}.{file.name.split('.')[-1]}"
|
||||
user.profile_picture.save(filename, ContentFile(file.read()))
|
||||
|
||||
return 200, {"message": "تصویر پروفایل با موفقیت بهروزرسانی شد."}
|
||||
|
||||
@auth_router.delete("/profile/picture", response={200: MessageSchema}, auth=jwt_auth)
|
||||
def delete_profile_picture(request):
|
||||
"""Delete current user's profile picture"""
|
||||
user = request.auth
|
||||
|
||||
if user.profile_picture:
|
||||
default_storage.delete(user.profile_picture.name)
|
||||
user.profile_picture = None
|
||||
user.save(update_fields=['profile_picture'])
|
||||
|
||||
return 200, {"message": "تصویر پروفایل با موفقیت حذف شد."}
|
||||
|
||||
@auth_router.post("/request-password-reset", response={200: MessageSchema, 400: ErrorSchema})
|
||||
def request_password_reset(request, data: PasswordResetRequestSchema):
|
||||
"""Request a password reset email"""
|
||||
try:
|
||||
user = get_object_or_404(User, email=data.email)
|
||||
user.set_password_reset_token()
|
||||
|
||||
reset_url = f"{settings.FRONTEND_PASSWORD_RESET_PAGE}/{user.password_reset_token}"
|
||||
send_password_reset_email.delay(user.id, reset_url)
|
||||
|
||||
# پیام عمومیِ یکسان برای جلوگیری از افشای وجود/عدم وجود ایمیل
|
||||
return 200, {"message": "اگر حسابی با این ایمیل وجود داشته باشد، لینک بازنشانی رمز عبور ارسال خواهد شد."}
|
||||
|
||||
except User.DoesNotExist:
|
||||
return 200, {"message": "اگر حسابی با این ایمیل وجود داشته باشد، لینک بازنشانی رمز عبور ارسال خواهد شد."}
|
||||
|
||||
except Exception as e:
|
||||
return 400, {"error": "درخواست بازنشانی رمز عبور انجام نشد.", "details": str(e)}
|
||||
|
||||
@auth_router.post("/reset-password-confirm", response={200: MessageSchema, 400: ErrorSchema})
|
||||
def reset_password_confirm(request, data: PasswordResetConfirmSchema):
|
||||
"""Confirm password reset with token and new password"""
|
||||
try:
|
||||
user = get_object_or_404(User, password_reset_token=data.token)
|
||||
|
||||
if user.password_reset_token_expires_at < timezone.now():
|
||||
user.password_reset_token = None
|
||||
user.password_reset_token_expires_at = None
|
||||
user.save(update_fields=['password_reset_token', 'password_reset_token_expires_at'])
|
||||
return 400, {"error": "زمان استفاده از لینک تغییر رمز عبور به پایان رسیده است. لطفاً دوباره اقدام کنید."}
|
||||
|
||||
user.set_password(data.new_password)
|
||||
user.password_reset_token = None
|
||||
user.password_reset_token_expires_at = None
|
||||
user.save(update_fields=['password', 'password_reset_token', 'password_reset_token_expires_at'])
|
||||
|
||||
return 200, {"message": "رمز عبور شما با موفقیت تغییر کرد."}
|
||||
|
||||
except User.DoesNotExist:
|
||||
return 400, {"error": "توکن بازنشانی رمز عبور نامعتبر یا منقضی شده است."}
|
||||
|
||||
except Exception as e:
|
||||
return 400, {"error": "تغییر رمز عبور انجام نشد.", "details": str(e)}
|
||||
|
||||
@auth_router.get("/users/deleted", response={200: List[UserProfileSchema], 403: ErrorSchema}, auth=jwt_auth)
|
||||
def list_deleted_users(request):
|
||||
"""List soft-deleted users via the dedicated manager (Admin/Committee only)."""
|
||||
if not (request.auth.is_staff or request.auth.is_superuser):
|
||||
return 403, {"error": "اجازه دسترسی ندارید."}
|
||||
|
||||
return User.deleted_objects.all()
|
||||
|
||||
@auth_router.post("/users/{user_id}/restore", response={200: MessageSchema, 400: ErrorSchema, 403: ErrorSchema}, auth=jwt_auth)
|
||||
def restore_user(request, user_id: int):
|
||||
"""Restore a soft-deleted user (Admin/Committee only)"""
|
||||
if not (request.auth.is_staff or request.auth.is_superuser):
|
||||
return 403, {"error": "اجازه دسترسی ندارید."}
|
||||
|
||||
try:
|
||||
user = User.deleted_objects.get(id=user_id)
|
||||
user.restore()
|
||||
return 200, {"message": f"کاربر {user.username} با موفقیت بازیابی شد."}
|
||||
except User.DoesNotExist:
|
||||
return 400, {"error": "کاربر یافت نشد یا حذف نرم نشده است."}
|
||||
except Exception as e:
|
||||
return 400, {"error": "بازیابی کاربر انجام نشد.", "details": str(e)}
|
||||
|
||||
@auth_router.get("/users", response={200: List[UserListSchema], 403: ErrorSchema}, auth=jwt_auth)
|
||||
def list_users(
|
||||
request,
|
||||
search: str | None = Query(None),
|
||||
role: str | None = Query(None, description="staff or superuser"),
|
||||
student_id: str | None = Query(None),
|
||||
university: str | None = Query(None),
|
||||
major: str | None = Query(None),
|
||||
is_active: str | None = Query(None, description="true or false"),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
user = request.auth
|
||||
if not (user.is_staff or user.is_superuser):
|
||||
return 403, {"error": "اجازه دسترسی ندارید."}
|
||||
|
||||
queryset = User.objects.order_by("-date_joined")
|
||||
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(username__icontains=search)
|
||||
| Q(email__icontains=search)
|
||||
| Q(first_name__icontains=search)
|
||||
| Q(last_name__icontains=search)
|
||||
)
|
||||
|
||||
if role == "staff":
|
||||
queryset = queryset.filter(is_staff=True)
|
||||
elif role == "superuser":
|
||||
queryset = queryset.filter(is_superuser=True)
|
||||
|
||||
if student_id:
|
||||
queryset = queryset.filter(student_id__icontains=student_id)
|
||||
|
||||
if university:
|
||||
queryset = queryset.filter(
|
||||
Q(university__code__icontains=university) | Q(university__name__icontains=university)
|
||||
)
|
||||
|
||||
if major:
|
||||
queryset = queryset.filter(
|
||||
Q(major__code__icontains=major) | Q(major__name__icontains=major)
|
||||
)
|
||||
|
||||
if is_active is not None:
|
||||
if is_active.lower() in ("true", "1"):
|
||||
queryset = queryset.filter(is_active=True)
|
||||
elif is_active.lower() in ("false", "0"):
|
||||
queryset = queryset.filter(is_active=False)
|
||||
|
||||
return queryset[offset : offset + limit]
|
||||
|
||||
@auth_router.get("/check-username", response=UsernameCheckSchema)
|
||||
def check_username_availability(request, username: str):
|
||||
"""Check if a username is available for registration"""
|
||||
exists = User.objects.filter(username=username).exists()
|
||||
return {"exists": exists}
|
||||
|
||||
9
apps/users/apps.py
Normal file
9
apps/users/apps.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.users"
|
||||
|
||||
def ready(self):
|
||||
import apps.users.signals
|
||||
48
apps/users/fixtures/agile.json
Normal file
48
apps/users/fixtures/agile.json
Normal file
@@ -0,0 +1,48 @@
|
||||
[
|
||||
{"model":"users.user","fields":{"username":"u1403020111029","email":"pending-1403020111029@noemail.local","first_name":"پوریا","last_name":"شامخی","student_id":"1403020111029","year_of_study":1403,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1400020111002","email":"pending-1400020111002@noemail.local","first_name":"سمانه","last_name":"جباری","student_id":"1400020111002","year_of_study":1400,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u990201110035","email":"pending-990201110035@noemail.local","first_name":"سید علی","last_name":"حجتی مقدم","student_id":"990201110035","year_of_study":1399,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u990201200032","email":"pending-990201200032@noemail.local","first_name":"مهدی","last_name":"خدیوی سرشت","student_id":"990201200032","year_of_study":1399,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020111026","email":"pending-1403020111026@noemail.local","first_name":"امیر سجاد","last_name":"حیدری","student_id":"1403020111026","year_of_study":1403,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020111037","email":"pending-1403020111037@noemail.local","first_name":"امیرکیان","last_name":"رادپور","student_id":"1403020111037","year_of_study":1403,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120011","email":"pending-1401020120011@noemail.local","first_name":"شیما","last_name":"گندمکار","student_id":"1401020120011","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120024","email":"pending-1401020120024@noemail.local","first_name":"رضا","last_name":"سالمیدرگاهی","student_id":"1401020120024","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120102","email":"pending-1401020120102@noemail.local","first_name":"امیرمحمد","last_name":"نیککار","student_id":"1401020120102","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120028","email":"pending-1401020120028@noemail.local","first_name":"امیرمحمد","last_name":"کیانفر","student_id":"1401020120028","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120035","email":"pending-1401020120035@noemail.local","first_name":"رژان","last_name":"پناهیپور","student_id":"1401020120035","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1400020111032","email":"pending-1400020111032@noemail.local","first_name":"مریم","last_name":"صفری","student_id":"1400020111032","year_of_study":1400,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1400020111014","email":"pending-1400020111014@noemail.local","first_name":"علیرضا","last_name":"رحیمی","student_id":"1400020111014","year_of_study":1400,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u992818200","email":"pending-992818200@noemail.local","first_name":"مریم","last_name":"مسلمی دوران محله","student_id":"992818200","year_of_study":1400,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1400020111022","email":"pending-1400020111022@noemail.local","first_name":"امیرمحمد","last_name":"خیراندیش","student_id":"1400020111022","year_of_study":1400,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1400020111029","email":"pending-1400020111029@noemail.local","first_name":"امیرحسین","last_name":"حسنپور","student_id":"1400020111029","year_of_study":1400,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u990201201007","email":"pending-990201201007@noemail.local","first_name":"امیررضا","last_name":"اخلاقی","student_id":"990201201007","year_of_study":1399,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020111006","email":"pending-1403020111006@noemail.local","first_name":"سینا","last_name":"زمانپور","student_id":"1403020111006","year_of_study":1403,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020130021","email":"pending-1403020130021@noemail.local","first_name":"سبحان","last_name":"آسوده جلالی","student_id":"1403020130021","year_of_study":1403,"major":null,"university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403012268121","email":"pending-1403012268121@noemail.local","first_name":"فربد","last_name":"خلیلی خوشه مهر","student_id":"1403012268121","year_of_study":1403,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u03111129302057","email":"pending-03111129302057@noemail.local","first_name":"محمد مهدی","last_name":"جباری","student_id":"03111129302057","year_of_study":1403,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020121009","email":"pending-1403020121009@noemail.local","first_name":"امیرحسین","last_name":"امینپور","student_id":"1403020121009","year_of_study":1403,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020121013","email":"pending-1403020121013@noemail.local","first_name":"عرشیا","last_name":"عرشی","student_id":"1403020121013","year_of_study":1403,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020121023","email":"pending-1403020121023@noemail.local","first_name":"طاها","last_name":"محیط مافی","student_id":"1403020121023","year_of_study":1403,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"uidx28","email":"pending-idx28@noemail.local","first_name":"مهدی","last_name":"منصورپور","student_id":null,"year_of_study":1403,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020121007","email":"pending-1403020121007@noemail.local","first_name":"سید محمدرضا","last_name":"حسیننیان","student_id":"1403020121007","year_of_study":1403,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020121001","email":"pending-1403020121001@noemail.local","first_name":"محمود","last_name":"یاسری","student_id":"1403020121001","year_of_study":1403,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120039","email":"pending-1401020120039@noemail.local","first_name":"ارشاد","last_name":"ایزدی","student_id":"1401020120039","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120002","email":"pending-1401020120002@noemail.local","first_name":"دلناز","last_name":"محمودی","student_id":"1401020120002","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020121018","email":"pending-1403020121018@noemail.local","first_name":"اروین","last_name":"نعمتی","student_id":"1403020121018","year_of_study":1403,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120149","email":"pending-1401020120149@noemail.local","first_name":"مائده","last_name":"حسرت قرانی","student_id":"1401020120149","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120036","email":"pending-1401020120036@noemail.local","first_name":"شهریار","last_name":"اقاجانی","student_id":"1401020120036","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020121027","email":"pending-1403020121027@noemail.local","first_name":"عمید","last_name":"عباسی","student_id":"1403020121027","year_of_study":1403,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u990201200016","email":"pending-990201200016@noemail.local","first_name":"مهدی","last_name":"دیداری","student_id":"990201200016","year_of_study":1399,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120041","email":"pending-1401020120041@noemail.local","first_name":"حمید","last_name":"عباسی","student_id":"1401020120041","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020130022","email":"pending-1403020130022@noemail.local","first_name":"امیرمحمد","last_name":"نجفی","student_id":"1403020130022","year_of_study":1403,"major":null,"university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020111049","email":"pending-1401020111049@noemail.local","first_name":"علی","last_name":"رهگذر","student_id":"1401020111049","year_of_study":1401,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120103","email":"pending-1401020120103@noemail.local","first_name":"یاسان","last_name":"حاجقلیزاده","student_id":"1401020120103","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"uidx45","email":"pending-idx45@noemail.local","first_name":"امیر","last_name":"دوستی ماسوله","student_id":null,"year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120031","email":"pending-1401020120031@noemail.local","first_name":"امیررضا","last_name":"علیپور","student_id":"1401020120031","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u990201200036","email":"pending-990201200036@noemail.local","first_name":"مونا","last_name":"یحییزاده واقفی","student_id":"990201200036","year_of_study":1399,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120005","email":"pending-1401020120005@noemail.local","first_name":"بهار","last_name":"محمدی","student_id":"1401020120005","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1401020120026","email":"pending-1401020120026@noemail.local","first_name":"مطهره","last_name":"حقشناس","student_id":"1401020120026","year_of_study":1401,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u1403020121020","email":"pending-1403020121020@noemail.local","first_name":"محمد","last_name":"خلیلیمقدم ملامحله","student_id":"1403020121020","year_of_study":1403,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"u990201200027","email":"pending-990201200027@noemail.local","first_name":"مهراب","last_name":"گودرزی","student_id":"990201200027","year_of_study":1399,"major":"IE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}},
|
||||
{"model":"users.user","fields":{"username":"uidx52","email":"pending-idx52@noemail.local","first_name":"امیرمحمد","last_name":"چرختاب مقدم","student_id":null,"year_of_study":1403,"major":"CE","university":"GILAN","is_email_verified":false,"password":"pbkdf2_sha256$390000$guilan$2FFrsLkTODb20Jf7oxtxgXXL1z6uQY8iCJJ2OGANkKk=","is_active":true,"created_at":"2025-01-01T00:00:00Z","updated_at":"2025-01-01T00:00:00Z"}}
|
||||
]
|
||||
244
apps/users/fixtures/users.json
Normal file
244
apps/users/fixtures/users.json
Normal file
@@ -0,0 +1,244 @@
|
||||
[
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"username": "admin",
|
||||
"email": "admin@cs-association.ac.ir",
|
||||
"first_name": "علی",
|
||||
"last_name": "احمدی",
|
||||
"student_id": "9812345001",
|
||||
"year_of_study": 4,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "رئیس انجمن علمی مهندسی کامپیوتر دانشگاه",
|
||||
"is_email_verified": true,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"is_staff": true,
|
||||
"is_superuser": true,
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-01T10:00:00Z",
|
||||
"updated_at": "2024-01-01T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"username": "sara_mohammadi",
|
||||
"email": "sara.mohammadi@student.ac.ir",
|
||||
"first_name": "سارا",
|
||||
"last_name": "محمدی",
|
||||
"student_id": "9912345002",
|
||||
"year_of_study": 3,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "نایب رئیس انجمن و مسئول رویدادها",
|
||||
"is_email_verified": true,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440002",
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-02T10:00:00Z",
|
||||
"updated_at": "2024-01-02T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"username": "reza_karimi",
|
||||
"email": "reza.karimi@student.ac.ir",
|
||||
"first_name": "رضا",
|
||||
"last_name": "کریمی",
|
||||
"student_id": "9912345003",
|
||||
"year_of_study": 2,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "علاقهمند به هوش مصنوعی و یادگیری ماشین",
|
||||
"is_email_verified": true,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-03T10:00:00Z",
|
||||
"updated_at": "2024-01-03T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"username": "maryam_hosseini",
|
||||
"email": "maryam.hosseini@student.ac.ir",
|
||||
"first_name": "مریم",
|
||||
"last_name": "حسینی",
|
||||
"student_id": "0012345004",
|
||||
"year_of_study": 1,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "دانشجوی سال اول و علاقهمند به برنامهنویسی وب",
|
||||
"is_email_verified": true,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440004",
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-04T10:00:00Z",
|
||||
"updated_at": "2024-01-04T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"username": "hassan_zare",
|
||||
"email": "hassan.zare@student.ac.ir",
|
||||
"first_name": "حسن",
|
||||
"last_name": "زارع",
|
||||
"student_id": "9812345005",
|
||||
"year_of_study": 4,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "مسئول روابط عمومی انجمن",
|
||||
"is_email_verified": true,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440005",
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-05T10:00:00Z",
|
||||
"updated_at": "2024-01-05T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"username": "zahra_safari",
|
||||
"email": "zahra.safari@student.ac.ir",
|
||||
"first_name": "زهرا",
|
||||
"last_name": "صفری",
|
||||
"student_id": "9912345006",
|
||||
"year_of_study": 3,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "علاقهمند به امنیت سایبری و شبکه",
|
||||
"is_email_verified": true,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440006",
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-06T10:00:00Z",
|
||||
"updated_at": "2024-01-06T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"username": "mohammad_rahmani",
|
||||
"email": "mohammad.rahmani@student.ac.ir",
|
||||
"first_name": "محمد",
|
||||
"last_name": "رحمانی",
|
||||
"student_id": "0012345007",
|
||||
"year_of_study": 1,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "دانشجوی جدید الورود",
|
||||
"is_email_verified": false,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440007",
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-07T10:00:00Z",
|
||||
"updated_at": "2024-01-07T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"username": "fateme_moradi",
|
||||
"email": "fateme.moradi@student.ac.ir",
|
||||
"first_name": "فاطمه",
|
||||
"last_name": "مرادی",
|
||||
"student_id": "9912345008",
|
||||
"year_of_study": 2,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "علاقهمند به توسعه اپلیکیشن موبایل",
|
||||
"is_email_verified": true,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440008",
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-08T10:00:00Z",
|
||||
"updated_at": "2024-01-08T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"username": "amir_ghorbani",
|
||||
"email": "amir.ghorbani@student.ac.ir",
|
||||
"first_name": "امیر",
|
||||
"last_name": "قربانی",
|
||||
"student_id": "9812345009",
|
||||
"year_of_study": 4,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "مسئول فنی انجمن و توسعهدهنده وبسایت",
|
||||
"is_email_verified": true,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440009",
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-09T10:00:00Z",
|
||||
"updated_at": "2024-01-09T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 10,
|
||||
"fields": {
|
||||
"username": "nasrin_jafari",
|
||||
"email": "nasrin.jafari@student.ac.ir",
|
||||
"first_name": "نسرین",
|
||||
"last_name": "جعفری",
|
||||
"student_id": "9912345010",
|
||||
"year_of_study": 3,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "علاقهمند به علم داده و تحلیل داده",
|
||||
"is_email_verified": true,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440010",
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-10T10:00:00Z",
|
||||
"updated_at": "2024-01-10T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 11,
|
||||
"fields": {
|
||||
"username": "mehdi_bagheri",
|
||||
"email": "mehdi.bagheri@student.ac.ir",
|
||||
"first_name": "مهدی",
|
||||
"last_name": "باقری",
|
||||
"student_id": "0012345011",
|
||||
"year_of_study": 1,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "دانشجوی سال اول و علاقهمند به بازیسازی",
|
||||
"is_email_verified": true,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440011",
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-11T10:00:00Z",
|
||||
"updated_at": "2024-01-11T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "users.user",
|
||||
"pk": 12,
|
||||
"fields": {
|
||||
"username": "leila_mousavi",
|
||||
"email": "leila.mousavi@student.ac.ir",
|
||||
"first_name": "لیلا",
|
||||
"last_name": "موسوی",
|
||||
"student_id": "9912345012",
|
||||
"year_of_study": 2,
|
||||
"major": "مهندسی کامپیوتر",
|
||||
"bio": "علاقهمند به طراحی UI/UX",
|
||||
"is_email_verified": true,
|
||||
"email_verification_token": "550e8400-e29b-41d4-a716-446655440012",
|
||||
"password": "pbkdf2_sha256$600000$test$test",
|
||||
"created_at": "2024-01-12T10:00:00Z",
|
||||
"updated_at": "2024-01-12T10:00:00Z",
|
||||
"is_deleted": false
|
||||
}
|
||||
}
|
||||
]
|
||||
60
apps/users/migrations/0001_initial.py
Normal file
60
apps/users/migrations/0001_initial.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Generated by Django 5.2.5 on 2025-10-16 12:07
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('email', models.EmailField(max_length=254, unique=True)),
|
||||
('bio', models.TextField(blank=True, null=True)),
|
||||
('profile_picture', models.ImageField(blank=True, null=True, upload_to='profile_pictures/')),
|
||||
('student_id', models.CharField(max_length=20, null=True)),
|
||||
('year_of_study', models.IntegerField(blank=True, null=True)),
|
||||
('major', models.CharField(blank=True, choices=[('CE', 'مهندسی کامپیوتر'), ('CS', 'علوم کامپیوتر'), ('SE', 'مهندسی نرم\u200cافزار'), ('IT', 'فناوری اطلاعات'), ('AI', 'هوش مصنوعی و رباتیک'), ('DATA', 'علم داده'), ('EE', 'مهندسی برق'), ('ME', 'مهندسی مکانیک'), ('CIV', 'مهندسی عمران'), ('CHE', 'مهندسی شیمی'), ('IE', 'مهندسی صنایع'), ('MSE', 'مهندسی مواد و متالورژی'), ('BME', 'مهندسی پزشکی'), ('ARCH', 'معماری'), ('AERO', 'مهندسی هوافضا'), ('PET', 'مهندسی نفت'), ('MIN', 'مهندسی معدن'), ('ENV', 'مهندسی محیط\u200cزیست'), ('URP', 'برنامه\u200cریزی شهری و منطقه\u200cای'), ('MATH', 'ریاضیات'), ('STAT', 'آمار'), ('PHYS', 'فیزیک'), ('CHEM', 'شیمی'), ('BIO', 'زیست\u200cشناسی'), ('GEO', 'زمین\u200cشناسی'), ('MED', 'پزشکی'), ('DEN', 'دندان\u200cپزشکی'), ('PHARM', 'داروسازی'), ('NURS', 'پرستاری'), ('MID', 'مامایی'), ('LAB', 'علوم آزمایشگاهی'), ('RAD', 'رادیولوژی'), ('ANES', 'بیهوشی'), ('PUBH', 'بهداشت'), ('AGRI', 'کشاورزی (عمومی)'), ('HORT', 'باغبانی'), ('PLP', 'گیاه\u200cپزشکی'), ('SOIL', 'علوم خاک'), ('VET', 'دامپزشکی'), ('MGT', 'مدیریت'), ('ACC', 'حسابداری'), ('FIN', 'مالی'), ('ECO', 'اقتصاد'), ('BA', 'مدیریت بازرگانی'), ('LAW', 'حقوق'), ('POL', 'علوم سیاسی'), ('SOC', 'جامعه\u200cشناسی'), ('PSY', 'روان\u200cشناسی'), ('PHIL', 'فلسفه'), ('HIST', 'تاریخ'), ('GEOG', 'جغرافیا'), ('EDU', 'علوم تربیتی'), ('PEd', 'تربیت بدنی'), ('LIT_FA', 'زبان و ادبیات فارسی'), ('LIT_EN', 'زبان و ادبیات انگلیسی'), ('LIT_AR', 'زبان و ادبیات عربی'), ('TRAN_EN', 'مترجمی زبان انگلیسی'), ('ART', 'هنرهای تجسمی'), ('GRAPH', 'گرافیک'), ('MUSIC', 'موسیقی'), ('THEAT', 'نمایش و تئاتر')], max_length=16, null=True)),
|
||||
('university', models.CharField(blank=True, choices=[('UT', 'دانشگاه تهران'), ('AUT', 'دانشگاه صنعتی امیرکبیر'), ('SHARIF', 'دانشگاه صنعتی شریف'), ('SBU', 'دانشگاه شهید بهشتی'), ('IUST', 'دانشگاه علم و صنعت ایران'), ('KNTU', 'دانشگاه صنعتی خواجه\u200cنصیر'), ('MODARES', 'دانشگاه تربیت مدرس'), ('ALLAMEH', 'دانشگاه علامه طباطبایی'), ('KHARAZMI', 'دانشگاه خوارزمی'), ('ISFAHAN_UNI', 'دانشگاه اصفهان'), ('IUT', 'دانشگاه صنعتی اصفهان'), ('SHIRAZ_UNI', 'دانشگاه شیراز'), ('TABRIZ_UNI', 'دانشگاه تبریز'), ('FERDOWSI', 'دانشگاه فردوسی مشهد'), ('RAZI', 'دانشگاه رازی'), ('BUALI', 'دانشگاه بوعلی\u200cسینا'), ('KURDISTAN', 'دانشگاه کردستان'), ('YAZD_UNI', 'دانشگاه یزد'), ('KERMAN_UNI', 'دانشگاه شهید باهنر کرمان'), ('MAZANDARAN', 'دانشگاه مازندران'), ('GILAN', 'دانشگاه گیلان'), ('GOLESTAN', 'دانشگاه گلستان'), ('URMIA', 'دانشگاه ارومیه'), ('ZANJAN', 'دانشگاه زنجان'), ('ARDABIL', 'دانشگاه محقق اردبیلی'), ('ARAK_UNI', 'دانشگاه اراک'), ('SEMNAN', 'دانشگاه سمنان'), ('SHAHROOD', 'دانشگاه صنعتی شاهرود'), ('QOM_UNI', 'دانشگاه قم'), ('IKIU', 'دانشگاه بین\u200cالمللی امام خمینی قزوین'), ('MAL_ASHTAR', 'دانشگاه صنعتی مالک\u200cاشتر'), ('SAHAND', 'دانشگاه صنعتی سهند'), ('BABOL_NOSH', 'دانشگاه صنعتی نوشیروانی بابل'), ('TOLOU', 'دانشگاه تحصیلات تکمیلی صنعتی و فناوری پیشرفته کرمان'), ('TUMS', 'دانشگاه علوم پزشکی تهران'), ('SBMU_MED', 'دانشگاه علوم پزشکی شهید بهشتی'), ('IUMS_MED', 'دانشگاه علوم پزشکی ایران'), ('MUMS_MED', 'دانشگاه علوم پزشکی مشهد'), ('SUMS_MED', 'دانشگاه علوم پزشکی شیراز'), ('TBZ_MED', 'دانشگاه علوم پزشکی تبریز'), ('ISF_MED', 'دانشگاه علوم پزشکی اصفهان'), ('AJA_MED', 'دانشگاه علوم پزشکی ارتش'), ('AJUMS_MED', 'دانشگاه علوم پزشکی اهواز'), ('KUMS_MED', 'دانشگاه علوم پزشکی کرمانشاه'), ('KER_MED', 'دانشگاه علوم پزشکی کرمان'), ('IAU_TEH', 'دانشگاه آزاد اسلامی تهران'), ('IAU_SCIRES', 'دانشگاه آزاد اسلامی علوم و تحقیقات تهران'), ('IAU_MASH', 'دانشگاه آزاد اسلامی مشهد'), ('IAU_TBRZ', 'دانشگاه آزاد اسلامی تبریز'), ('IAU_SHIR', 'دانشگاه آزاد اسلامی شیراز'), ('IAU_ISF', 'دانشگاه آزاد اسلامی اصفهان (خوراسگان)'), ('IAU_RASHT', 'دانشگاه آزاد اسلامی رشت'), ('IAU_QAZ', 'دانشگاه آزاد اسلامی قزوین'), ('PNU_TEH', 'دانشگاه پیام نور تهران'), ('PNU_RAS', 'دانشگاه پیام نور رشت'), ('PNU_MASH', 'دانشگاه پیام نور مشهد'), ('PNU_TBRZ', 'دانشگاه پیام نور تبریز'), ('UAST_TEH', 'دانشگاه علمی-کاربردی تهران'), ('UAST_GIL', 'دانشگاه علمی-کاربردی گیلان'), ('TVU_TEH', 'دانشگاه فنی و حرفه\u200cای تهران'), ('TVU_GIL', 'دانشگاه فنی و حرفه\u200cای گیلان'), ('RAJAEI', 'دانشگاه تربیت دبیر شهید رجایی'), ('IMAM_SADEQ', 'دانشگاه امام صادق (ع)'), ('ART_TEH', 'دانشگاه هنر'), ('TEH_MARK', 'دانشگاه علامه محدث نوری/علامه طباطبایی (در صورت نیاز اصلاح کنید)')], max_length=16, null=True)),
|
||||
('is_email_verified', models.BooleanField(default=False)),
|
||||
('email_verification_token', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('email_verification_sent_at', models.DateTimeField(blank=True, null=True)),
|
||||
('password_reset_token', models.UUIDField(blank=True, null=True, unique=True)),
|
||||
('password_reset_token_expires_at', models.DateTimeField(blank=True, null=True)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User',
|
||||
'verbose_name_plural': 'Users',
|
||||
'db_table': 'users',
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
apps/users/migrations/0002_alter_user_university.py
Normal file
18
apps/users/migrations/0002_alter_user_university.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.5 on 2025-10-18 10:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='university',
|
||||
field=models.CharField(blank=True, choices=[('UT', 'دانشگاه تهران'), ('AUT', 'دانشگاه صنعتی امیرکبیر'), ('SHARIF', 'دانشگاه صنعتی شریف'), ('SBU', 'دانشگاه شهید بهشتی'), ('IUST', 'دانشگاه علم و صنعت ایران'), ('KNTU', 'دانشگاه صنعتی خواجه\u200cنصیر'), ('MODARES', 'دانشگاه تربیت مدرس'), ('ALLAMEH', 'دانشگاه علامه طباطبایی'), ('KHARAZMI', 'دانشگاه خوارزمی'), ('ISFAHAN_UNI', 'دانشگاه اصفهان'), ('IUT', 'دانشگاه صنعتی اصفهان'), ('SHIRAZ_UNI', 'دانشگاه شیراز'), ('TABRIZ_UNI', 'دانشگاه تبریز'), ('FERDOWSI', 'دانشگاه فردوسی مشهد'), ('RAZI', 'دانشگاه رازی'), ('BUALI', 'دانشگاه بوعلی\u200cسینا'), ('KURDISTAN', 'دانشگاه کردستان'), ('YAZD_UNI', 'دانشگاه یزد'), ('KERMAN_UNI', 'دانشگاه شهید باهنر کرمان'), ('MAZANDARAN', 'دانشگاه مازندران'), ('GILAN', 'دانشگاه گیلان'), ('GOLESTAN', 'دانشگاه گلستان'), ('URMIA', 'دانشگاه ارومیه'), ('ZANJAN', 'دانشگاه زنجان'), ('ARDABIL', 'دانشگاه محقق اردبیلی'), ('ARAK_UNI', 'دانشگاه اراک'), ('SEMNAN', 'دانشگاه سمنان'), ('SHAHROOD', 'دانشگاه صنعتی شاهرود'), ('QOM_UNI', 'دانشگاه قم'), ('IKIU', 'دانشگاه بین\u200cالمللی امام خمینی قزوین'), ('MAL_ASHTAR', 'دانشگاه صنعتی مالک\u200cاشتر'), ('SAHAND', 'دانشگاه صنعتی سهند'), ('BABOL_NOSH', 'دانشگاه صنعتی نوشیروانی بابل'), ('TOLOU', 'دانشگاه تحصیلات تکمیلی صنعتی و فناوری پیشرفته کرمان'), ('TUMS', 'دانشگاه علوم پزشکی تهران'), ('SBMU_MED', 'دانشگاه علوم پزشکی شهید بهشتی'), ('IUMS_MED', 'دانشگاه علوم پزشکی ایران'), ('MUMS_MED', 'دانشگاه علوم پزشکی مشهد'), ('SUMS_MED', 'دانشگاه علوم پزشکی شیراز'), ('TBZ_MED', 'دانشگاه علوم پزشکی تبریز'), ('ISF_MED', 'دانشگاه علوم پزشکی اصفهان'), ('AJA_MED', 'دانشگاه علوم پزشکی ارتش'), ('AJUMS_MED', 'دانشگاه علوم پزشکی اهواز'), ('KUMS_MED', 'دانشگاه علوم پزشکی کرمانشاه'), ('KER_MED', 'دانشگاه علوم پزشکی کرمان'), ('IAU_TEH', 'دانشگاه آزاد اسلامی تهران'), ('IAU_SCIRES', 'دانشگاه آزاد اسلامی علوم و تحقیقات تهران'), ('IAU_MASH', 'دانشگاه آزاد اسلامی مشهد'), ('IAU_TBRZ', 'دانشگاه آزاد اسلامی تبریز'), ('IAU_SHIR', 'دانشگاه آزاد اسلامی شیراز'), ('IAU_ISF', 'دانشگاه آزاد اسلامی اصفهان'), ('IAU_RASHT', 'دانشگاه آزاد اسلامی رشت'), ('IAU_LAHIJAN', 'دانشگاه آزاد اسلامی لاهیجان'), ('IAU_QAZ', 'دانشگاه آزاد اسلامی قزوین'), ('PNU_TEH', 'دانشگاه پیام نور تهران'), ('PNU_RAS', 'دانشگاه پیام نور رشت'), ('PNU_MASH', 'دانشگاه پیام نور مشهد'), ('PNU_TBRZ', 'دانشگاه پیام نور تبریز'), ('UAST_TEH', 'دانشگاه علمی-کاربردی تهران'), ('UAST_GIL', 'دانشگاه علمی-کاربردی گیلان'), ('TVU_TEH', 'دانشگاه فنی و حرفه\u200cای تهران'), ('TVU_GIL', 'دانشگاه فنی و حرفه\u200cای گیلان'), ('RAJAEI', 'دانشگاه تربیت دبیر شهید رجایی'), ('IMAM_SADEQ', 'دانشگاه امام صادق (ع)'), ('ART_TEH', 'دانشگاه هنر')], max_length=127, null=True),
|
||||
),
|
||||
]
|
||||
18
apps/users/migrations/0003_alter_user_university.py
Normal file
18
apps/users/migrations/0003_alter_user_university.py
Normal file
File diff suppressed because one or more lines are too long
372
apps/users/migrations/0004_major_university_models.py
Normal file
372
apps/users/migrations/0004_major_university_models.py
Normal file
@@ -0,0 +1,372 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
MAJOR_CHOICES = [('CE', 'مهندسی کامپیوتر'),
|
||||
('CS', 'علوم کامپیوتر'),
|
||||
('SE', 'مهندسی نرم\u200cافزار'),
|
||||
('IT', 'فناوری اطلاعات'),
|
||||
('AI', 'هوش مصنوعی و رباتیک'),
|
||||
('DATA', 'علم داده'),
|
||||
('EE', 'مهندسی برق'),
|
||||
('ME', 'مهندسی مکانیک'),
|
||||
('CIV', 'مهندسی عمران'),
|
||||
('CHE', 'مهندسی شیمی'),
|
||||
('IE', 'مهندسی صنایع'),
|
||||
('MSE', 'مهندسی مواد و متالورژی'),
|
||||
('BME', 'مهندسی پزشکی'),
|
||||
('ARCH', 'معماری'),
|
||||
('AERO', 'مهندسی هوافضا'),
|
||||
('PET', 'مهندسی نفت'),
|
||||
('MIN', 'مهندسی معدن'),
|
||||
('ENV', 'مهندسی محیط\u200cزیست'),
|
||||
('URP', 'برنامه\u200cریزی شهری و منطقه\u200cای'),
|
||||
('MATH', 'ریاضیات'),
|
||||
('STAT', 'آمار'),
|
||||
('PHYS', 'فیزیک'),
|
||||
('CHEM', 'شیمی'),
|
||||
('BIO', 'زیست\u200cشناسی'),
|
||||
('GEO', 'زمین\u200cشناسی'),
|
||||
('MED', 'پزشکی'),
|
||||
('DEN', 'دندان\u200cپزشکی'),
|
||||
('PHARM', 'داروسازی'),
|
||||
('NURS', 'پرستاری'),
|
||||
('MID', 'مامایی'),
|
||||
('LAB', 'علوم آزمایشگاهی'),
|
||||
('RAD', 'رادیولوژی'),
|
||||
('ANES', 'بیهوشی'),
|
||||
('PUBH', 'بهداشت'),
|
||||
('AGRI', 'کشاورزی (عمومی)'),
|
||||
('HORT', 'باغبانی'),
|
||||
('PLP', 'گیاه\u200cپزشکی'),
|
||||
('SOIL', 'علوم خاک'),
|
||||
('VET', 'دامپزشکی'),
|
||||
('MGT', 'مدیریت'),
|
||||
('ACC', 'حسابداری'),
|
||||
('FIN', 'مالی'),
|
||||
('ECO', 'اقتصاد'),
|
||||
('BA', 'مدیریت بازرگانی'),
|
||||
('LAW', 'حقوق'),
|
||||
('POL', 'علوم سیاسی'),
|
||||
('SOC', 'جامعه\u200cشناسی'),
|
||||
('PSY', 'روان\u200cشناسی'),
|
||||
('PHIL', 'فلسفه'),
|
||||
('HIST', 'تاریخ'),
|
||||
('GEOG', 'جغرافیا'),
|
||||
('EDU', 'علوم تربیتی'),
|
||||
('LIT_FA', 'زبان و ادبیات فارسی'),
|
||||
('LIT_EN', 'زبان و ادبیات انگلیسی'),
|
||||
('LIT_AR', 'زبان و ادبیات عربی'),
|
||||
('TRAN_EN', 'مترجمی زبان انگلیسی'),
|
||||
('ART', 'هنرهای تجسمی'),
|
||||
('GRAPH', 'گرافیک'),
|
||||
('MUSIC', 'موسیقی'),
|
||||
('THEAT', 'نمایش و تئاتر')]
|
||||
|
||||
UNIVERSITY_CHOICES = [('GILAN', 'دانشگاه گیلان'),
|
||||
('UT', 'دانشگاه تهران'),
|
||||
('AUT', 'دانشگاه صنعتی امیرکبیر'),
|
||||
('SHARIF', 'دانشگاه صنعتی شریف'),
|
||||
('SBU', 'دانشگاه شهید بهشتی'),
|
||||
('IUST', 'دانشگاه علم و صنعت ایران'),
|
||||
('KNTU', 'دانشگاه صنعتی خواجه\u200cنصیر'),
|
||||
('MODARES', 'دانشگاه تربیت مدرس'),
|
||||
('ALLAMEH', 'دانشگاه علامه طباطبایی'),
|
||||
('KHARAZMI', 'دانشگاه خوارزمی'),
|
||||
('ISFAHAN_UNI', 'دانشگاه اصفهان'),
|
||||
('IUT', 'دانشگاه صنعتی اصفهان'),
|
||||
('SHIRAZ_UNI', 'دانشگاه شیراز'),
|
||||
('SHIRAZ_TECH', 'دانشگاه صنعتی شیراز'),
|
||||
('TABRIZ_UNI', 'دانشگاه تبریز'),
|
||||
('FERDOWSI', 'دانشگاه فردوسی مشهد'),
|
||||
('IMAMREZA', 'دانشگاه بین المللی امام رضا مشهد'),
|
||||
('RAZI', 'دانشگاه رازی'),
|
||||
('SHAHRKORD', 'دانشگاه شهرکرد'),
|
||||
('BUALI', 'دانشگاه بوعلی\u200cسینا'),
|
||||
('KURDISTAN', 'دانشگاه کردستان'),
|
||||
('YAZD_UNI', 'دانشگاه یزد'),
|
||||
('KERMAN_UNI', 'دانشگاه شهید باهنر کرمان'),
|
||||
('MAZANDARAN', 'دانشگاه مازندران'),
|
||||
('GOLESTAN', 'دانشگاه گلستان'),
|
||||
('URMIA', 'دانشگاه ارومیه'),
|
||||
('ZANJAN', 'دانشگاه زنجان'),
|
||||
('ARDABIL', 'دانشگاه محقق اردبیلی'),
|
||||
('SEMNAN', 'دانشگاه سمنان'),
|
||||
('SHAHROOD', 'دانشگاه صنعتی شاهرود'),
|
||||
('QOM_UNI', 'دانشگاه قم'),
|
||||
('QOM_TECH', 'دانشگاه صنعتی قم'),
|
||||
('IKIU', 'دانشگاه بین\u200cالمللی امام خمینی قزوین'),
|
||||
('MAL_ASHTAR', 'دانشگاه صنعتی مالک\u200cاشتر'),
|
||||
('SAHAND', 'دانشگاه صنعتی سهند'),
|
||||
('BABOL_NOSH', 'دانشگاه صنعتی نوشیروانی بابل'),
|
||||
('BIRGAND', 'دانشگاه بیرجند'),
|
||||
('ALZAHRA', 'دانشگاه الزهرا'),
|
||||
('TAFRESH', 'دانشگاه تفرش'),
|
||||
('JAHROM', 'دانشگاه جهرم'),
|
||||
('HAKIM_SABZ', 'دانشگاه حکیم سبزواری'),
|
||||
('PERSIAN_GULF', 'دانشگاه خلیج فارس'),
|
||||
('DAMGHAN', 'دانشگاه دامغان'),
|
||||
('ILAM', 'دانشگاه ایلام'),
|
||||
('BOJNORD', 'دانشگاه بجنورد'),
|
||||
('KASHAN', 'دانشگاه کاشان'),
|
||||
('LORESTAN', 'دانشگاه لرستان'),
|
||||
('MARAGHEH', 'دانشگاه مراغه'),
|
||||
('MALAYER', 'دانشگاه ملایر'),
|
||||
('NEYSHABUR', 'دانشگاه نیشابور'),
|
||||
('HORMOZGAN', 'دانشگاه هرمزگان'),
|
||||
('HONAR', 'دانشگاه هنر'),
|
||||
('TUMS', 'دانشگاه علوم پزشکی تهران'),
|
||||
('SBMU_MED', 'دانشگاه علوم پزشکی شهید بهشتی'),
|
||||
('IUMS_MED', 'دانشگاه علوم پزشکی ایران'),
|
||||
('MUMS_MED', 'دانشگاه علوم پزشکی مشهد'),
|
||||
('SUMS_MED', 'دانشگاه علوم پزشکی شیراز'),
|
||||
('TBZ_MED', 'دانشگاه علوم پزشکی تبریز'),
|
||||
('ISF_MED', 'دانشگاه علوم پزشکی اصفهان'),
|
||||
('AJUMS_MED', 'دانشگاه علوم پزشکی اهواز'),
|
||||
('AJA_MED', 'دانشگاه علوم پزشکی ارتش'),
|
||||
('KUMS_MED', 'دانشگاه علوم پزشکی کرمانشاه'),
|
||||
('KER_MED', 'دانشگاه علوم پزشکی کرمان'),
|
||||
('MED_QOM', 'دانشگاه علوم پزشکی قم'),
|
||||
('MED_QAZVIN', 'دانشگاه علوم پزشکی قزوین'),
|
||||
('MED_ALBORZ', 'دانشگاه علوم پزشکی البرز'),
|
||||
('MED_ARAK', 'دانشگاه علوم پزشکی اراک'),
|
||||
('MED_ZANJAN', 'دانشگاه علوم پزشکی زنجان'),
|
||||
('MED_MAZANDARAN', 'دانشگاه علوم پزشکی مازندران'),
|
||||
('MED_BABOL', 'دانشگاه علوم پزشکی بابل'),
|
||||
('MED_GOLESTAN', 'دانشگاه علوم پزشکی گلستان'),
|
||||
('MED_GILAN', 'دانشگاه علوم پزشکی گیلان'),
|
||||
('MED_HORMOZGAN', 'دانشگاه علوم پزشکی هرمزگان'),
|
||||
('MED_BUSHEHR', 'دانشگاه علوم پزشکی بوشهر'),
|
||||
('MED_BIRJAND', 'دانشگاه علوم پزشکی بیرجند'),
|
||||
('MED_BOJNORD', 'دانشگاه علوم پزشکی خراسان شمالی (بجنورد)'),
|
||||
('MED_SABZEVAR', 'دانشگاه علوم پزشکی سبزوار'),
|
||||
('MED_NEYSHABUR', 'دانشگاه علوم پزشکی نیشابور'),
|
||||
('MED_GONABAD', 'دانشگاه علوم پزشکی گناباد'),
|
||||
('MED_SHAHROUD', 'دانشگاه علوم پزشکی شاهرود'),
|
||||
('MED_SEMNAN', 'دانشگاه علوم پزشکی سمنان'),
|
||||
('MED_YAZD', 'دانشگاه علوم پزشکی یزد'),
|
||||
('MED_URMIA', 'دانشگاه علوم پزشکی ارومیه'),
|
||||
('MED_ARDABIL', 'دانشگاه علوم پزشکی اردبیل'),
|
||||
('MED_HAMEDAN', 'دانشگاه علوم پزشکی همدان'),
|
||||
('MED_LARESTAN', 'دانشکده علوم پزشکی لارستان'),
|
||||
('MED_FASA', 'دانشگاه علوم پزشکی فسا'),
|
||||
('MED_JAHROM', 'دانشگاه علوم پزشکی جهرم'),
|
||||
('MED_KASHAN', 'دانشگاه علوم پزشکی کاشان'),
|
||||
('MED_ILAM', 'دانشگاه علوم پزشکی ایلام'),
|
||||
('MED_LORESTAN', 'دانشگاه علوم پزشکی لرستان'),
|
||||
('MED_KHUZESTAN', 'دانشگاه علوم پزشکی دزفول/شوشتر (استان خوزستان)'),
|
||||
('IAU_TEH_CENTRAL', 'دانشگاه آزاد اسلامی واحد تهران مرکزی'),
|
||||
('IAU_TEH_NORTH', 'دانشگاه آزاد اسلامی واحد تهران شمال'),
|
||||
('IAU_TEH_SOUTH', 'دانشگاه آزاد اسلامی واحد تهران جنوب'),
|
||||
('IAU_TEH_WEST', 'دانشگاه آزاد اسلامی واحد تهران غرب'),
|
||||
('IAU_TEH_EAST', 'دانشگاه آزاد اسلامی واحد تهران شرق'),
|
||||
('IAU_SRT_TEHRAN', 'دانشگاه آزاد اسلامی واحد علوم و تحقیقات تهران'),
|
||||
('IAU_QAZVIN', 'دانشگاه آزاد اسلامی قزوین'),
|
||||
('IAU_NAJAFABAD', 'دانشگاه آزاد اسلامی نجف\u200cآباد'),
|
||||
('IAU_MASHHAD', 'دانشگاه آزاد اسلامی مشهد'),
|
||||
('IAU_TABRIZ', 'دانشگاه آزاد اسلامی تبریز'),
|
||||
('IAU_SHIRAZ', 'دانشگاه آزاد اسلامی شیراز'),
|
||||
('IAU_ISFAHAN', 'دانشگاه آزاد اسلامی اصفهان (خوراسگان)'),
|
||||
('IAU_KARAJ', 'دانشگاه آزاد اسلامی کرج'),
|
||||
('IAU_QOM', 'دانشگاه آزاد اسلامی قم'),
|
||||
('IAU_RASHT', 'دانشگاه آزاد اسلامی رشت'),
|
||||
('IAU_LAHIJAN', 'دانشگاه آزاد اسلامی لاهیجان'),
|
||||
('IAU_SARI', 'دانشگاه آزاد اسلامی ساری'),
|
||||
('IAU_YAZD', 'دانشگاه آزاد اسلامی یزد'),
|
||||
('IAU_KERMAN', 'دانشگاه آزاد اسلامی کرمان'),
|
||||
('IAU_BANDARABBAS', 'دانشگاه آزاد اسلامی بندرعباس'),
|
||||
('IAU_BUSHEHR', 'دانشگاه آزاد اسلامی بوشهر'),
|
||||
('IAU_AHVAZ', 'دانشگاه آزاد اسلامی اهواز'),
|
||||
('IAU_KHORRAMABAD', 'دانشگاه آزاد اسلامی خرم\u200cآباد'),
|
||||
('IAU_SANANDAJ', 'دانشگاه آزاد اسلامی سنندج'),
|
||||
('IAU_HAMEDAN', 'دانشگاه آزاد اسلامی همدان'),
|
||||
('IAU_ARAK', 'دانشگاه آزاد اسلامی اراک'),
|
||||
('IAU_URMIA', 'دانشگاه آزاد اسلامی ارومیه'),
|
||||
('IAU_ZANJAN', 'دانشگاه آزاد اسلامی زنجان'),
|
||||
('IAU_BIRJAND', 'دانشگاه آزاد اسلامی بیرجند'),
|
||||
('IAU_BOJNORD', 'دانشگاه آزاد اسلامی بجنورد'),
|
||||
('IAU_SEMNAN', 'دانشگاه آزاد اسلامی سمنان'),
|
||||
('IAU_GORGAN', 'دانشگاه آزاد اسلامی گرگان'),
|
||||
('IAU_MARVDASHT', 'دانشگاه آزاد اسلامی مرودشت'),
|
||||
('IAU_KISH_INTL', 'دانشگاه آزاد اسلامی بین\u200cالملل کیش'),
|
||||
('IAU_QESHM_INTL', 'دانشگاه آزاد اسلامی قشم (بین\u200cالملل)'),
|
||||
('PNU_EAST_AZERBAIJAN', 'دانشگاه پیام نور آذربایجان شرقی'),
|
||||
('PNU_WEST_AZERBAIJAN', 'دانشگاه پیام نور آذربایجان غربی'),
|
||||
('PNU_ARDABIL', 'دانشگاه پیام نور اردبیل'),
|
||||
('PNU_ISFAHAN', 'دانشگاه پیام نور اصفهان'),
|
||||
('PNU_ALBORZ', 'دانشگاه پیام نور البرز'),
|
||||
('PNU_ILAM', 'دانشگاه پیام نور ایلام'),
|
||||
('PNU_BUSHEHR', 'دانشگاه پیام نور بوشهر'),
|
||||
('PNU_TEHRAN', 'دانشگاه پیام نور تهران'),
|
||||
('PNU_CH_BAKHTIARI', 'دانشگاه پیام نور چهارمحال و بختیاری'),
|
||||
('PNU_SOUTH_KHORASAN', 'دانشگاه پیام نور خراسان جنوبی'),
|
||||
('PNU_RAZAVI_KHORASAN', 'دانشگاه پیام نور خراسان رضوی'),
|
||||
('PNU_NORTH_KHORASAN', 'دانشگاه پیام نور خراسان شمالی'),
|
||||
('PNU_KHUZESTAN', 'دانشگاه پیام نور خوزستان'),
|
||||
('PNU_ZANJAN', 'دانشگاه پیام نور زنجان'),
|
||||
('PNU_SEMNAN', 'دانشگاه پیام نور سمنان'),
|
||||
('PNU_SISTAN_BALUCH', 'دانشگاه پیام نور سیستان و بلوچستان'),
|
||||
('PNU_FARS', 'دانشگاه پیام نور فارس'),
|
||||
('PNU_QAZVIN', 'دانشگاه پیام نور قزوین'),
|
||||
('PNU_QOM', 'دانشگاه پیام نور قم'),
|
||||
('PNU_KURDISTAN', 'دانشگاه پیام نور کردستان'),
|
||||
('PNU_KERMAN', 'دانشگاه پیام نور کرمان'),
|
||||
('PNU_KERMANSHAH', 'دانشگاه پیام نور کرمانشاه'),
|
||||
('PNU_KOHGILUYEH', 'دانشگاه پیام نور کهگیلویه و بویراحمد'),
|
||||
('PNU_GOLESTAN', 'دانشگاه پیام نور گلستان'),
|
||||
('PNU_GILAN', 'دانشگاه پیام نور گیلان'),
|
||||
('PNU_LORESTAN', 'دانشگاه پیام نور لرستان'),
|
||||
('PNU_MAZANDARAN', 'دانشگاه پیام نور مازندران'),
|
||||
('PNU_MARKAZI', 'دانشگاه پیام نور مرکزی'),
|
||||
('PNU_HORMOZGAN', 'دانشگاه پیام نور هرمزگان'),
|
||||
('PNU_HAMEDAN', 'دانشگاه پیام نور همدان'),
|
||||
('PNU_YAZD', 'دانشگاه پیام نور یزد'),
|
||||
('UAST_EAST_AZERBAIJAN', 'دانشگاه جامع علمی کاربردی آذربایجان شرقی'),
|
||||
('UAST_WEST_AZERBAIJAN', 'دانشگاه جامع علمی کاربردی آذربایجان غربی'),
|
||||
('UAST_ARDABIL', 'دانشگاه جامع علمی کاربردی اردبیل'),
|
||||
('UAST_ISFAHAN', 'دانشگاه جامع علمی کاربردی اصفهان'),
|
||||
('UAST_ALBORZ', 'دانشگاه جامع علمی کاربردی البرز'),
|
||||
('UAST_ILAM', 'دانشگاه جامع علمی کاربردی ایلام'),
|
||||
('UAST_BUSHEHR', 'دانشگاه جامع علمی کاربردی بوشهر'),
|
||||
('UAST_TEHRAN', 'دانشگاه جامع علمی کاربردی تهران'),
|
||||
('UAST_CH_BAKHTIARI', 'دانشگاه جامع علمی کاربردی چهارمحال و بختیاری'),
|
||||
('UAST_SOUTH_KHORASAN', 'دانشگاه جامع علمی کاربردی خراسان جنوبی'),
|
||||
('UAST_RAZAVI_KHORASAN', 'دانشگاه جامع علمی کاربردی خراسان رضوی'),
|
||||
('UAST_NORTH_KHORASAN', 'دانشگاه جامع علمی کاربردی خراسان شمالی'),
|
||||
('UAST_KHUZESTAN', 'دانشگاه جامع علمی کاربردی خوزستان'),
|
||||
('UAST_ZANJAN', 'دانشگاه جامع علمی کاربردی زنجان'),
|
||||
('UAST_SEMNAN', 'دانشگاه جامع علمی کاربردی سمنان'),
|
||||
('UAST_SISTAN_BALUCH', 'دانشگاه جامع علمی کاربردی سیستان و بلوچستان'),
|
||||
('UAST_FARS', 'دانشگاه جامع علمی کاربردی فارس'),
|
||||
('UAST_QAZVIN', 'دانشگاه جامع علمی کاربردی قزوین'),
|
||||
('UAST_QOM', 'دانشگاه جامع علمی کاربردی قم'),
|
||||
('UAST_KURDISTAN', 'دانشگاه جامع علمی کاربردی کردستان'),
|
||||
('UAST_KERMAN', 'دانشگاه جامع علمی کاربردی کرمان'),
|
||||
('UAST_KERMANSHAH', 'دانشگاه جامع علمی کاربردی کرمانشاه'),
|
||||
('UAST_KOHGILUYEH', 'دانشگاه جامع علمی کاربردی کهگیلویه و بویراحمد'),
|
||||
('UAST_GOLESTAN', 'دانشگاه جامع علمی کاربردی گلستان'),
|
||||
('UAST_GILAN', 'دانشگاه جامع علمی کاربردی گیلان'),
|
||||
('UAST_LORESTAN', 'دانشگاه جامع علمی کاربردی لرستان'),
|
||||
('UAST_MAZANDARAN', 'دانشگاه جامع علمی کاربردی مازندران'),
|
||||
('UAST_MARKAZI', 'دانشگاه جامع علمی کاربردی مرکزی'),
|
||||
('UAST_HORMOZGAN', 'دانشگاه جامع علمی کاربردی هرمزگان'),
|
||||
('UAST_HAMEDAN', 'دانشگاه جامع علمی کاربردی همدان'),
|
||||
('UAST_YAZD', 'دانشگاه جامع علمی کاربردی یزد'),
|
||||
('SCIENCE_CULTURE', 'دانشگاه علم و فرهنگ'),
|
||||
('KHATAM', 'دانشگاه خاتم'),
|
||||
('SOOREH', 'دانشگاه سوره'),
|
||||
('MOFID', 'دانشگاه مفید'),
|
||||
('SHOMAL', 'دانشگاه شمال'),
|
||||
('QURANIC_UNI', 'دانشگاه علوم و معارف قرآن کریم')]
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0003_alter_user_university"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="University",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("is_deleted", models.BooleanField(default=False)),
|
||||
("deleted_at", models.DateTimeField(blank=True, null=True)),
|
||||
("code", models.CharField(max_length=64, unique=True)),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
"ordering": ["name"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Major",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("is_deleted", models.BooleanField(default=False)),
|
||||
("deleted_at", models.DateTimeField(blank=True, null=True)),
|
||||
("code", models.CharField(max_length=64, unique=True)),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
"ordering": ["name"],
|
||||
},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="user",
|
||||
old_name="major",
|
||||
new_name="legacy_major",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="user",
|
||||
old_name="university",
|
||||
new_name="legacy_university",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="legacy_major",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=MAJOR_CHOICES,
|
||||
editable=False,
|
||||
max_length=16,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="legacy_university",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=UNIVERSITY_CHOICES,
|
||||
editable=False,
|
||||
max_length=127,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="major",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="users",
|
||||
to="users.major",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="university",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="users",
|
||||
to="users.university",
|
||||
),
|
||||
),
|
||||
]
|
||||
316
apps/users/migrations/0005_populate_major_university.py
Normal file
316
apps/users/migrations/0005_populate_major_university.py
Normal file
@@ -0,0 +1,316 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
MAJOR_CHOICES = [('CE', 'مهندسی کامپیوتر'),
|
||||
('CS', 'علوم کامپیوتر'),
|
||||
('SE', 'مهندسی نرم\u200cافزار'),
|
||||
('IT', 'فناوری اطلاعات'),
|
||||
('AI', 'هوش مصنوعی و رباتیک'),
|
||||
('DATA', 'علم داده'),
|
||||
('EE', 'مهندسی برق'),
|
||||
('ME', 'مهندسی مکانیک'),
|
||||
('CIV', 'مهندسی عمران'),
|
||||
('CHE', 'مهندسی شیمی'),
|
||||
('IE', 'مهندسی صنایع'),
|
||||
('MSE', 'مهندسی مواد و متالورژی'),
|
||||
('BME', 'مهندسی پزشکی'),
|
||||
('ARCH', 'معماری'),
|
||||
('AERO', 'مهندسی هوافضا'),
|
||||
('PET', 'مهندسی نفت'),
|
||||
('MIN', 'مهندسی معدن'),
|
||||
('ENV', 'مهندسی محیط\u200cزیست'),
|
||||
('URP', 'برنامه\u200cریزی شهری و منطقه\u200cای'),
|
||||
('MATH', 'ریاضیات'),
|
||||
('STAT', 'آمار'),
|
||||
('PHYS', 'فیزیک'),
|
||||
('CHEM', 'شیمی'),
|
||||
('BIO', 'زیست\u200cشناسی'),
|
||||
('GEO', 'زمین\u200cشناسی'),
|
||||
('MED', 'پزشکی'),
|
||||
('DEN', 'دندان\u200cپزشکی'),
|
||||
('PHARM', 'داروسازی'),
|
||||
('NURS', 'پرستاری'),
|
||||
('MID', 'مامایی'),
|
||||
('LAB', 'علوم آزمایشگاهی'),
|
||||
('RAD', 'رادیولوژی'),
|
||||
('ANES', 'بیهوشی'),
|
||||
('PUBH', 'بهداشت'),
|
||||
('AGRI', 'کشاورزی (عمومی)'),
|
||||
('HORT', 'باغبانی'),
|
||||
('PLP', 'گیاه\u200cپزشکی'),
|
||||
('SOIL', 'علوم خاک'),
|
||||
('VET', 'دامپزشکی'),
|
||||
('MGT', 'مدیریت'),
|
||||
('ACC', 'حسابداری'),
|
||||
('FIN', 'مالی'),
|
||||
('ECO', 'اقتصاد'),
|
||||
('BA', 'مدیریت بازرگانی'),
|
||||
('LAW', 'حقوق'),
|
||||
('POL', 'علوم سیاسی'),
|
||||
('SOC', 'جامعه\u200cشناسی'),
|
||||
('PSY', 'روان\u200cشناسی'),
|
||||
('PHIL', 'فلسفه'),
|
||||
('HIST', 'تاریخ'),
|
||||
('GEOG', 'جغرافیا'),
|
||||
('EDU', 'علوم تربیتی'),
|
||||
('LIT_FA', 'زبان و ادبیات فارسی'),
|
||||
('LIT_EN', 'زبان و ادبیات انگلیسی'),
|
||||
('LIT_AR', 'زبان و ادبیات عربی'),
|
||||
('TRAN_EN', 'مترجمی زبان انگلیسی'),
|
||||
('ART', 'هنرهای تجسمی'),
|
||||
('GRAPH', 'گرافیک'),
|
||||
('MUSIC', 'موسیقی'),
|
||||
('THEAT', 'نمایش و تئاتر')]
|
||||
|
||||
UNIVERSITY_CHOICES = [('GILAN', 'دانشگاه گیلان'),
|
||||
('UT', 'دانشگاه تهران'),
|
||||
('AUT', 'دانشگاه صنعتی امیرکبیر'),
|
||||
('SHARIF', 'دانشگاه صنعتی شریف'),
|
||||
('SBU', 'دانشگاه شهید بهشتی'),
|
||||
('IUST', 'دانشگاه علم و صنعت ایران'),
|
||||
('KNTU', 'دانشگاه صنعتی خواجه\u200cنصیر'),
|
||||
('MODARES', 'دانشگاه تربیت مدرس'),
|
||||
('ALLAMEH', 'دانشگاه علامه طباطبایی'),
|
||||
('KHARAZMI', 'دانشگاه خوارزمی'),
|
||||
('ISFAHAN_UNI', 'دانشگاه اصفهان'),
|
||||
('IUT', 'دانشگاه صنعتی اصفهان'),
|
||||
('SHIRAZ_UNI', 'دانشگاه شیراز'),
|
||||
('SHIRAZ_TECH', 'دانشگاه صنعتی شیراز'),
|
||||
('TABRIZ_UNI', 'دانشگاه تبریز'),
|
||||
('FERDOWSI', 'دانشگاه فردوسی مشهد'),
|
||||
('IMAMREZA', 'دانشگاه بین المللی امام رضا مشهد'),
|
||||
('RAZI', 'دانشگاه رازی'),
|
||||
('SHAHRKORD', 'دانشگاه شهرکرد'),
|
||||
('BUALI', 'دانشگاه بوعلی\u200cسینا'),
|
||||
('KURDISTAN', 'دانشگاه کردستان'),
|
||||
('YAZD_UNI', 'دانشگاه یزد'),
|
||||
('KERMAN_UNI', 'دانشگاه شهید باهنر کرمان'),
|
||||
('MAZANDARAN', 'دانشگاه مازندران'),
|
||||
('GOLESTAN', 'دانشگاه گلستان'),
|
||||
('URMIA', 'دانشگاه ارومیه'),
|
||||
('ZANJAN', 'دانشگاه زنجان'),
|
||||
('ARDABIL', 'دانشگاه محقق اردبیلی'),
|
||||
('SEMNAN', 'دانشگاه سمنان'),
|
||||
('SHAHROOD', 'دانشگاه صنعتی شاهرود'),
|
||||
('QOM_UNI', 'دانشگاه قم'),
|
||||
('QOM_TECH', 'دانشگاه صنعتی قم'),
|
||||
('IKIU', 'دانشگاه بین\u200cالمللی امام خمینی قزوین'),
|
||||
('MAL_ASHTAR', 'دانشگاه صنعتی مالک\u200cاشتر'),
|
||||
('SAHAND', 'دانشگاه صنعتی سهند'),
|
||||
('BABOL_NOSH', 'دانشگاه صنعتی نوشیروانی بابل'),
|
||||
('BIRGAND', 'دانشگاه بیرجند'),
|
||||
('ALZAHRA', 'دانشگاه الزهرا'),
|
||||
('TAFRESH', 'دانشگاه تفرش'),
|
||||
('JAHROM', 'دانشگاه جهرم'),
|
||||
('HAKIM_SABZ', 'دانشگاه حکیم سبزواری'),
|
||||
('PERSIAN_GULF', 'دانشگاه خلیج فارس'),
|
||||
('DAMGHAN', 'دانشگاه دامغان'),
|
||||
('ILAM', 'دانشگاه ایلام'),
|
||||
('BOJNORD', 'دانشگاه بجنورد'),
|
||||
('KASHAN', 'دانشگاه کاشان'),
|
||||
('LORESTAN', 'دانشگاه لرستان'),
|
||||
('MARAGHEH', 'دانشگاه مراغه'),
|
||||
('MALAYER', 'دانشگاه ملایر'),
|
||||
('NEYSHABUR', 'دانشگاه نیشابور'),
|
||||
('HORMOZGAN', 'دانشگاه هرمزگان'),
|
||||
('HONAR', 'دانشگاه هنر'),
|
||||
('TUMS', 'دانشگاه علوم پزشکی تهران'),
|
||||
('SBMU_MED', 'دانشگاه علوم پزشکی شهید بهشتی'),
|
||||
('IUMS_MED', 'دانشگاه علوم پزشکی ایران'),
|
||||
('MUMS_MED', 'دانشگاه علوم پزشکی مشهد'),
|
||||
('SUMS_MED', 'دانشگاه علوم پزشکی شیراز'),
|
||||
('TBZ_MED', 'دانشگاه علوم پزشکی تبریز'),
|
||||
('ISF_MED', 'دانشگاه علوم پزشکی اصفهان'),
|
||||
('AJUMS_MED', 'دانشگاه علوم پزشکی اهواز'),
|
||||
('AJA_MED', 'دانشگاه علوم پزشکی ارتش'),
|
||||
('KUMS_MED', 'دانشگاه علوم پزشکی کرمانشاه'),
|
||||
('KER_MED', 'دانشگاه علوم پزشکی کرمان'),
|
||||
('MED_QOM', 'دانشگاه علوم پزشکی قم'),
|
||||
('MED_QAZVIN', 'دانشگاه علوم پزشکی قزوین'),
|
||||
('MED_ALBORZ', 'دانشگاه علوم پزشکی البرز'),
|
||||
('MED_ARAK', 'دانشگاه علوم پزشکی اراک'),
|
||||
('MED_ZANJAN', 'دانشگاه علوم پزشکی زنجان'),
|
||||
('MED_MAZANDARAN', 'دانشگاه علوم پزشکی مازندران'),
|
||||
('MED_BABOL', 'دانشگاه علوم پزشکی بابل'),
|
||||
('MED_GOLESTAN', 'دانشگاه علوم پزشکی گلستان'),
|
||||
('MED_GILAN', 'دانشگاه علوم پزشکی گیلان'),
|
||||
('MED_HORMOZGAN', 'دانشگاه علوم پزشکی هرمزگان'),
|
||||
('MED_BUSHEHR', 'دانشگاه علوم پزشکی بوشهر'),
|
||||
('MED_BIRJAND', 'دانشگاه علوم پزشکی بیرجند'),
|
||||
('MED_BOJNORD', 'دانشگاه علوم پزشکی خراسان شمالی (بجنورد)'),
|
||||
('MED_SABZEVAR', 'دانشگاه علوم پزشکی سبزوار'),
|
||||
('MED_NEYSHABUR', 'دانشگاه علوم پزشکی نیشابور'),
|
||||
('MED_GONABAD', 'دانشگاه علوم پزشکی گناباد'),
|
||||
('MED_SHAHROUD', 'دانشگاه علوم پزشکی شاهرود'),
|
||||
('MED_SEMNAN', 'دانشگاه علوم پزشکی سمنان'),
|
||||
('MED_YAZD', 'دانشگاه علوم پزشکی یزد'),
|
||||
('MED_URMIA', 'دانشگاه علوم پزشکی ارومیه'),
|
||||
('MED_ARDABIL', 'دانشگاه علوم پزشکی اردبیل'),
|
||||
('MED_HAMEDAN', 'دانشگاه علوم پزشکی همدان'),
|
||||
('MED_LARESTAN', 'دانشکده علوم پزشکی لارستان'),
|
||||
('MED_FASA', 'دانشگاه علوم پزشکی فسا'),
|
||||
('MED_JAHROM', 'دانشگاه علوم پزشکی جهرم'),
|
||||
('MED_KASHAN', 'دانشگاه علوم پزشکی کاشان'),
|
||||
('MED_ILAM', 'دانشگاه علوم پزشکی ایلام'),
|
||||
('MED_LORESTAN', 'دانشگاه علوم پزشکی لرستان'),
|
||||
('MED_KHUZESTAN', 'دانشگاه علوم پزشکی دزفول/شوشتر (استان خوزستان)'),
|
||||
('IAU_TEH_CENTRAL', 'دانشگاه آزاد اسلامی واحد تهران مرکزی'),
|
||||
('IAU_TEH_NORTH', 'دانشگاه آزاد اسلامی واحد تهران شمال'),
|
||||
('IAU_TEH_SOUTH', 'دانشگاه آزاد اسلامی واحد تهران جنوب'),
|
||||
('IAU_TEH_WEST', 'دانشگاه آزاد اسلامی واحد تهران غرب'),
|
||||
('IAU_TEH_EAST', 'دانشگاه آزاد اسلامی واحد تهران شرق'),
|
||||
('IAU_SRT_TEHRAN', 'دانشگاه آزاد اسلامی واحد علوم و تحقیقات تهران'),
|
||||
('IAU_QAZVIN', 'دانشگاه آزاد اسلامی قزوین'),
|
||||
('IAU_NAJAFABAD', 'دانشگاه آزاد اسلامی نجف\u200cآباد'),
|
||||
('IAU_MASHHAD', 'دانشگاه آزاد اسلامی مشهد'),
|
||||
('IAU_TABRIZ', 'دانشگاه آزاد اسلامی تبریز'),
|
||||
('IAU_SHIRAZ', 'دانشگاه آزاد اسلامی شیراز'),
|
||||
('IAU_ISFAHAN', 'دانشگاه آزاد اسلامی اصفهان (خوراسگان)'),
|
||||
('IAU_KARAJ', 'دانشگاه آزاد اسلامی کرج'),
|
||||
('IAU_QOM', 'دانشگاه آزاد اسلامی قم'),
|
||||
('IAU_RASHT', 'دانشگاه آزاد اسلامی رشت'),
|
||||
('IAU_LAHIJAN', 'دانشگاه آزاد اسلامی لاهیجان'),
|
||||
('IAU_SARI', 'دانشگاه آزاد اسلامی ساری'),
|
||||
('IAU_YAZD', 'دانشگاه آزاد اسلامی یزد'),
|
||||
('IAU_KERMAN', 'دانشگاه آزاد اسلامی کرمان'),
|
||||
('IAU_BANDARABBAS', 'دانشگاه آزاد اسلامی بندرعباس'),
|
||||
('IAU_BUSHEHR', 'دانشگاه آزاد اسلامی بوشهر'),
|
||||
('IAU_AHVAZ', 'دانشگاه آزاد اسلامی اهواز'),
|
||||
('IAU_KHORRAMABAD', 'دانشگاه آزاد اسلامی خرم\u200cآباد'),
|
||||
('IAU_SANANDAJ', 'دانشگاه آزاد اسلامی سنندج'),
|
||||
('IAU_HAMEDAN', 'دانشگاه آزاد اسلامی همدان'),
|
||||
('IAU_ARAK', 'دانشگاه آزاد اسلامی اراک'),
|
||||
('IAU_URMIA', 'دانشگاه آزاد اسلامی ارومیه'),
|
||||
('IAU_ZANJAN', 'دانشگاه آزاد اسلامی زنجان'),
|
||||
('IAU_BIRJAND', 'دانشگاه آزاد اسلامی بیرجند'),
|
||||
('IAU_BOJNORD', 'دانشگاه آزاد اسلامی بجنورد'),
|
||||
('IAU_SEMNAN', 'دانشگاه آزاد اسلامی سمنان'),
|
||||
('IAU_GORGAN', 'دانشگاه آزاد اسلامی گرگان'),
|
||||
('IAU_MARVDASHT', 'دانشگاه آزاد اسلامی مرودشت'),
|
||||
('IAU_KISH_INTL', 'دانشگاه آزاد اسلامی بین\u200cالملل کیش'),
|
||||
('IAU_QESHM_INTL', 'دانشگاه آزاد اسلامی قشم (بین\u200cالملل)'),
|
||||
('PNU_EAST_AZERBAIJAN', 'دانشگاه پیام نور آذربایجان شرقی'),
|
||||
('PNU_WEST_AZERBAIJAN', 'دانشگاه پیام نور آذربایجان غربی'),
|
||||
('PNU_ARDABIL', 'دانشگاه پیام نور اردبیل'),
|
||||
('PNU_ISFAHAN', 'دانشگاه پیام نور اصفهان'),
|
||||
('PNU_ALBORZ', 'دانشگاه پیام نور البرز'),
|
||||
('PNU_ILAM', 'دانشگاه پیام نور ایلام'),
|
||||
('PNU_BUSHEHR', 'دانشگاه پیام نور بوشهر'),
|
||||
('PNU_TEHRAN', 'دانشگاه پیام نور تهران'),
|
||||
('PNU_CH_BAKHTIARI', 'دانشگاه پیام نور چهارمحال و بختیاری'),
|
||||
('PNU_SOUTH_KHORASAN', 'دانشگاه پیام نور خراسان جنوبی'),
|
||||
('PNU_RAZAVI_KHORASAN', 'دانشگاه پیام نور خراسان رضوی'),
|
||||
('PNU_NORTH_KHORASAN', 'دانشگاه پیام نور خراسان شمالی'),
|
||||
('PNU_KHUZESTAN', 'دانشگاه پیام نور خوزستان'),
|
||||
('PNU_ZANJAN', 'دانشگاه پیام نور زنجان'),
|
||||
('PNU_SEMNAN', 'دانشگاه پیام نور سمنان'),
|
||||
('PNU_SISTAN_BALUCH', 'دانشگاه پیام نور سیستان و بلوچستان'),
|
||||
('PNU_FARS', 'دانشگاه پیام نور فارس'),
|
||||
('PNU_QAZVIN', 'دانشگاه پیام نور قزوین'),
|
||||
('PNU_QOM', 'دانشگاه پیام نور قم'),
|
||||
('PNU_KURDISTAN', 'دانشگاه پیام نور کردستان'),
|
||||
('PNU_KERMAN', 'دانشگاه پیام نور کرمان'),
|
||||
('PNU_KERMANSHAH', 'دانشگاه پیام نور کرمانشاه'),
|
||||
('PNU_KOHGILUYEH', 'دانشگاه پیام نور کهگیلویه و بویراحمد'),
|
||||
('PNU_GOLESTAN', 'دانشگاه پیام نور گلستان'),
|
||||
('PNU_GILAN', 'دانشگاه پیام نور گیلان'),
|
||||
('PNU_LORESTAN', 'دانشگاه پیام نور لرستان'),
|
||||
('PNU_MAZANDARAN', 'دانشگاه پیام نور مازندران'),
|
||||
('PNU_MARKAZI', 'دانشگاه پیام نور مرکزی'),
|
||||
('PNU_HORMOZGAN', 'دانشگاه پیام نور هرمزگان'),
|
||||
('PNU_HAMEDAN', 'دانشگاه پیام نور همدان'),
|
||||
('PNU_YAZD', 'دانشگاه پیام نور یزد'),
|
||||
('UAST_EAST_AZERBAIJAN', 'دانشگاه جامع علمی کاربردی آذربایجان شرقی'),
|
||||
('UAST_WEST_AZERBAIJAN', 'دانشگاه جامع علمی کاربردی آذربایجان غربی'),
|
||||
('UAST_ARDABIL', 'دانشگاه جامع علمی کاربردی اردبیل'),
|
||||
('UAST_ISFAHAN', 'دانشگاه جامع علمی کاربردی اصفهان'),
|
||||
('UAST_ALBORZ', 'دانشگاه جامع علمی کاربردی البرز'),
|
||||
('UAST_ILAM', 'دانشگاه جامع علمی کاربردی ایلام'),
|
||||
('UAST_BUSHEHR', 'دانشگاه جامع علمی کاربردی بوشهر'),
|
||||
('UAST_TEHRAN', 'دانشگاه جامع علمی کاربردی تهران'),
|
||||
('UAST_CH_BAKHTIARI', 'دانشگاه جامع علمی کاربردی چهارمحال و بختیاری'),
|
||||
('UAST_SOUTH_KHORASAN', 'دانشگاه جامع علمی کاربردی خراسان جنوبی'),
|
||||
('UAST_RAZAVI_KHORASAN', 'دانشگاه جامع علمی کاربردی خراسان رضوی'),
|
||||
('UAST_NORTH_KHORASAN', 'دانشگاه جامع علمی کاربردی خراسان شمالی'),
|
||||
('UAST_KHUZESTAN', 'دانشگاه جامع علمی کاربردی خوزستان'),
|
||||
('UAST_ZANJAN', 'دانشگاه جامع علمی کاربردی زنجان'),
|
||||
('UAST_SEMNAN', 'دانشگاه جامع علمی کاربردی سمنان'),
|
||||
('UAST_SISTAN_BALUCH', 'دانشگاه جامع علمی کاربردی سیستان و بلوچستان'),
|
||||
('UAST_FARS', 'دانشگاه جامع علمی کاربردی فارس'),
|
||||
('UAST_QAZVIN', 'دانشگاه جامع علمی کاربردی قزوین'),
|
||||
('UAST_QOM', 'دانشگاه جامع علمی کاربردی قم'),
|
||||
('UAST_KURDISTAN', 'دانشگاه جامع علمی کاربردی کردستان'),
|
||||
('UAST_KERMAN', 'دانشگاه جامع علمی کاربردی کرمان'),
|
||||
('UAST_KERMANSHAH', 'دانشگاه جامع علمی کاربردی کرمانشاه'),
|
||||
('UAST_KOHGILUYEH', 'دانشگاه جامع علمی کاربردی کهگیلویه و بویراحمد'),
|
||||
('UAST_GOLESTAN', 'دانشگاه جامع علمی کاربردی گلستان'),
|
||||
('UAST_GILAN', 'دانشگاه جامع علمی کاربردی گیلان'),
|
||||
('UAST_LORESTAN', 'دانشگاه جامع علمی کاربردی لرستان'),
|
||||
('UAST_MAZANDARAN', 'دانشگاه جامع علمی کاربردی مازندران'),
|
||||
('UAST_MARKAZI', 'دانشگاه جامع علمی کاربردی مرکزی'),
|
||||
('UAST_HORMOZGAN', 'دانشگاه جامع علمی کاربردی هرمزگان'),
|
||||
('UAST_HAMEDAN', 'دانشگاه جامع علمی کاربردی همدان'),
|
||||
('UAST_YAZD', 'دانشگاه جامع علمی کاربردی یزد'),
|
||||
('SCIENCE_CULTURE', 'دانشگاه علم و فرهنگ'),
|
||||
('KHATAM', 'دانشگاه خاتم'),
|
||||
('SOOREH', 'دانشگاه سوره'),
|
||||
('MOFID', 'دانشگاه مفید'),
|
||||
('SHOMAL', 'دانشگاه شمال'),
|
||||
('QURANIC_UNI', 'دانشگاه علوم و معارف قرآن کریم')]
|
||||
|
||||
|
||||
def seed_reference_models(apps, schema_editor):
|
||||
Major = apps.get_model("users", "Major")
|
||||
University = apps.get_model("users", "University")
|
||||
User = apps.get_model("users", "User")
|
||||
|
||||
major_map = {}
|
||||
for code, label in MAJOR_CHOICES:
|
||||
obj, _ = Major.objects.update_or_create(
|
||||
code=code,
|
||||
defaults={"name": label},
|
||||
)
|
||||
major_map[code] = obj
|
||||
|
||||
university_map = {}
|
||||
for code, label in UNIVERSITY_CHOICES:
|
||||
obj, _ = University.objects.update_or_create(
|
||||
code=code,
|
||||
defaults={"name": label},
|
||||
)
|
||||
university_map[code] = obj
|
||||
|
||||
users = User.objects.all()
|
||||
for user in users.iterator():
|
||||
updates = []
|
||||
major_code = getattr(user, "legacy_major", None)
|
||||
if major_code:
|
||||
major = major_map.get(major_code)
|
||||
if major and user.major_id != major.id:
|
||||
user.major_id = major.id
|
||||
updates.append("major")
|
||||
|
||||
university_code = getattr(user, "legacy_university", None)
|
||||
if university_code:
|
||||
uni = university_map.get(university_code)
|
||||
if uni and user.university_id != uni.id:
|
||||
user.university_id = uni.id
|
||||
updates.append("university")
|
||||
|
||||
if updates:
|
||||
user.save(update_fields=updates)
|
||||
|
||||
|
||||
def noop(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0004_major_university_models"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(seed_reference_models, noop),
|
||||
]
|
||||
19
apps/users/migrations/0006_remove_legacy_fields.py
Normal file
19
apps/users/migrations/0006_remove_legacy_fields.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0005_populate_major_university"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="legacy_major",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="legacy_university",
|
||||
),
|
||||
]
|
||||
0
apps/users/migrations/__init__.py
Normal file
0
apps/users/migrations/__init__.py
Normal file
112
apps/users/models.py
Normal file
112
apps/users/models.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.utils import timezone
|
||||
from django.db import models
|
||||
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from core.models import BaseModel
|
||||
|
||||
|
||||
class University(BaseModel):
|
||||
code = models.CharField(max_length=64, unique=True)
|
||||
name = models.CharField(max_length=255)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Major(BaseModel):
|
||||
code = models.CharField(max_length=64, unique=True)
|
||||
name = models.CharField(max_length=255)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class User(AbstractUser, BaseModel):
|
||||
email = models.EmailField(unique=True)
|
||||
bio = models.TextField(null=True, blank=True)
|
||||
profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True)
|
||||
|
||||
student_id = models.CharField(max_length=20, null=True)
|
||||
year_of_study = models.IntegerField(null=True, blank=True)
|
||||
major = models.ForeignKey(
|
||||
Major,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='users',
|
||||
)
|
||||
university = models.ForeignKey(
|
||||
University,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='users',
|
||||
)
|
||||
is_email_verified = models.BooleanField(default=False)
|
||||
email_verification_token = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
email_verification_sent_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
password_reset_token = models.UUIDField(null=True, blank=True, unique=True)
|
||||
password_reset_token_expires_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = ['username']
|
||||
|
||||
class Meta:
|
||||
db_table = 'users'
|
||||
verbose_name = 'User'
|
||||
verbose_name_plural = 'Users'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_full_name()} ({self.email})"
|
||||
|
||||
def get_full_name(self):
|
||||
return f"{self.first_name} {self.last_name}".strip()
|
||||
|
||||
def get_major_display(self):
|
||||
if self.major:
|
||||
return self.major.name
|
||||
return None
|
||||
|
||||
def get_university_display(self):
|
||||
if self.university:
|
||||
return self.university.name
|
||||
return None
|
||||
|
||||
def regenerate_verification_token(self):
|
||||
self.email_verification_token = uuid.uuid4()
|
||||
self.save(update_fields=['email_verification_token'])
|
||||
|
||||
def set_password_reset_token(self):
|
||||
"""Generates a new password reset token and sets its expiry."""
|
||||
self.password_reset_token = uuid.uuid4()
|
||||
self.password_reset_token_expires_at = timezone.now() + timedelta(hours=1)
|
||||
self.save(update_fields=['password_reset_token', 'password_reset_token_expires_at'])
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
send_verified_success = False
|
||||
|
||||
if self.pk is not None:
|
||||
prev = type(self).objects.filter(pk=self.pk).values_list('is_email_verified', flat=True).first()
|
||||
if prev is not None and prev is False and self.is_email_verified is True:
|
||||
send_verified_success = True
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if send_verified_success:
|
||||
try:
|
||||
from apps.users.tasks import send_email_verified_success
|
||||
send_email_verified_success.delay(self.id)
|
||||
except Exception:
|
||||
pass
|
||||
29
apps/users/resources.py
Normal file
29
apps/users/resources.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from import_export import resources, fields
|
||||
from import_export.widgets import BooleanWidget
|
||||
|
||||
from apps.users.models import User
|
||||
|
||||
class UserResource(resources.ModelResource):
|
||||
is_staff = fields.Field(
|
||||
column_name='is_staff',
|
||||
attribute='is_staff',
|
||||
widget=BooleanWidget()
|
||||
)
|
||||
is_superuser = fields.Field(
|
||||
column_name='is_superuser',
|
||||
attribute='is_superuser',
|
||||
widget=BooleanWidget()
|
||||
)
|
||||
is_email_verified = fields.Field(
|
||||
column_name='is_email_verified',
|
||||
attribute='is_email_verified',
|
||||
widget=BooleanWidget()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('id', 'username', 'email', 'first_name', 'last_name',
|
||||
'student_id', 'year_of_study', 'major',
|
||||
'is_staff', 'is_superuser',
|
||||
'is_email_verified', 'bio')
|
||||
export_order = fields
|
||||
27
apps/users/signals.py
Normal file
27
apps/users/signals.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import uuid
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
from apps.users.models import User
|
||||
from apps.users.tasks import send_verification_email
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def send_verification_email_on_registration(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
if not instance.username:
|
||||
instance.username = str(uuid.uuid4())[:10]
|
||||
instance.save(update_fields=['username'])
|
||||
|
||||
if not instance.is_email_verified and instance.email:
|
||||
# Update the email verification sent timestamp
|
||||
instance.email_verification_sent_at = timezone.now()
|
||||
instance.save(update_fields=['email_verification_sent_at'])
|
||||
|
||||
# Generate verification URL (you'll need to adjust this based on your frontend)
|
||||
verification_url = f"{settings.FRONTEND_ROOT}verify-email/{instance.email_verification_token}"
|
||||
|
||||
# Send verification email asynchronously
|
||||
send_verification_email.delay(instance.id, verification_url)
|
||||
99
apps/users/tasks.py
Normal file
99
apps/users/tasks.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.conf import settings
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
from celery import shared_task
|
||||
import logging
|
||||
|
||||
from apps.users.models import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def send_verification_email(self, user_id, verification_url):
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
|
||||
subject = 'تایید ایمیل | انجمن علمی مهندسی کامپیوتر'
|
||||
html_message = render_to_string('emails/verification_email.html', {
|
||||
'user': user,
|
||||
'verification_url': verification_url,
|
||||
})
|
||||
plain_message = strip_tags(html_message)
|
||||
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=plain_message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[user.email],
|
||||
html_message=html_message,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
logger.info(f"Verification email sent to {user.email}")
|
||||
return f"Verification email sent to {user.email}"
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Failed to send verification email: {exc}")
|
||||
raise self.retry(exc=exc, countdown=60)
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def send_password_reset_email(self, user_id, reset_url):
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
|
||||
subject = 'بازیابی رمز عبور | انجمن علمی مهندسی کامپیوتر'
|
||||
html_message = render_to_string('emails/password_reset_email.html', {
|
||||
'user': user,
|
||||
'reset_url': reset_url,
|
||||
})
|
||||
plain_message = strip_tags(html_message)
|
||||
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=plain_message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[user.email],
|
||||
html_message=html_message,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
logger.info(f"Password reset email sent to {user.email}")
|
||||
return f"Password reset email sent to {user.email}"
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Failed to send password reset email: {exc}")
|
||||
raise self.retry(exc=exc, countdown=60)
|
||||
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def send_email_verified_success(self, user_id: int):
|
||||
"""
|
||||
ارسال ایمیل «ایمیل شما با موفقیت تأیید شد» پس از تغییر وضعیت تأیید.
|
||||
"""
|
||||
try:
|
||||
user = User.objects.get(pk=user_id)
|
||||
|
||||
subject = "تأیید ایمیل شما با موفقیت انجام شد"
|
||||
context = {
|
||||
"user": user,
|
||||
"home_url": getattr(settings, "FRONTEND_ROOT", "/"),
|
||||
}
|
||||
html_message = render_to_string("emails/verification_success.html", context)
|
||||
plain_message = strip_tags(html_message)
|
||||
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=plain_message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[user.email],
|
||||
html_message=html_message,
|
||||
fail_silently=False,
|
||||
)
|
||||
logger.info(f"verified success email sent to {user.email}")
|
||||
return f"verified success email sent to {user.email}"
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Failed to send verified success email: {exc}")
|
||||
raise self.retry(exc=exc, countdown=60)
|
||||
0
apps/users/tests/__init__.py
Normal file
0
apps/users/tests/__init__.py
Normal file
0
apps/users/tests/integration/__init__.py
Normal file
0
apps/users/tests/integration/__init__.py
Normal file
724
apps/users/tests/integration/test_users.py
Normal file
724
apps/users/tests/integration/test_users.py
Normal file
@@ -0,0 +1,724 @@
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from unittest import mock
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
import jwt
|
||||
|
||||
from apps.users.models import User, Major, University
|
||||
|
||||
|
||||
class UsersAPIIntegrationTests(TestCase):
|
||||
password = "Sup3rSecure!123"
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super().setUpTestData()
|
||||
cls.major_cs, _ = Major.objects.get_or_create(
|
||||
code="CS", defaults={"name": "Computer Science"}
|
||||
)
|
||||
cls.major_gil, _ = Major.objects.get_or_create(
|
||||
code="GIL_CS", defaults={"name": "Gilan Computer Science"}
|
||||
)
|
||||
cls.university_ut, _ = University.objects.get_or_create(
|
||||
code="UT", defaults={"name": "University of Tehran"}
|
||||
)
|
||||
cls.university_gilan, _ = University.objects.get_or_create(
|
||||
code="GILAN", defaults={"name": "Gilan University"}
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
patchers = [
|
||||
mock.patch("apps.users.tasks.send_verification_email.delay"),
|
||||
mock.patch("apps.users.signals.send_verification_email.delay"),
|
||||
mock.patch("apps.users.tasks.send_password_reset_email.delay"),
|
||||
]
|
||||
(
|
||||
self.mock_send_verification_task,
|
||||
self.mock_signal_verification_task,
|
||||
self.mock_password_reset_task,
|
||||
) = [patcher.start() for patcher in patchers]
|
||||
for patcher in patchers:
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
# Helper utilities -----------------------------------------------------
|
||||
|
||||
def _numeric_student_id(self) -> str:
|
||||
return str(uuid.uuid4().int)[-10:]
|
||||
|
||||
def _resolve_major(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, Major):
|
||||
return value
|
||||
return Major.objects.filter(code=value).first()
|
||||
|
||||
def _resolve_university(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, University):
|
||||
return value
|
||||
return University.objects.filter(code=value).first()
|
||||
|
||||
def _create_user(self, **overrides) -> User:
|
||||
unique = uuid.uuid4().hex[:8]
|
||||
defaults = {
|
||||
"username": f"user_{unique}",
|
||||
"email": f"{unique}@example.com",
|
||||
"student_id": self._numeric_student_id(),
|
||||
"first_name": "Test",
|
||||
"last_name": "User",
|
||||
"year_of_study": 2,
|
||||
"major": self.major_cs,
|
||||
"university": self.university_ut,
|
||||
}
|
||||
defaults.update(overrides)
|
||||
if isinstance(defaults.get("major"), str):
|
||||
defaults["major"] = self._resolve_major(defaults["major"])
|
||||
if isinstance(defaults.get("university"), str):
|
||||
defaults["university"] = self._resolve_university(defaults["university"])
|
||||
password = defaults.pop("password", self.password)
|
||||
return User.objects.create_user(password=password, **defaults)
|
||||
|
||||
def _auth_headers(self, token: str) -> dict:
|
||||
return {"HTTP_AUTHORIZATION": f"Bearer {token}"}
|
||||
|
||||
def _login_and_get_tokens(self, user: User, password: str | None = None) -> dict:
|
||||
response = self.client.post(
|
||||
"/api/auth/login",
|
||||
data=json.dumps({"email": user.email, "password": password or self.password}),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return response.json()
|
||||
|
||||
def _refresh_token_value(self, user: User | None = None, **overrides) -> str:
|
||||
now = timezone.now()
|
||||
payload = {
|
||||
"type": "refresh",
|
||||
"exp": now + timedelta(minutes=5),
|
||||
"iat": now,
|
||||
}
|
||||
if user is not None:
|
||||
payload["user_id"] = user.id
|
||||
payload.update(overrides)
|
||||
return jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
|
||||
|
||||
# Registration ---------------------------------------------------------
|
||||
|
||||
def test_register_creates_user_and_enqueues_signal(self):
|
||||
# Arrange
|
||||
payload = {
|
||||
"username": "integration_user",
|
||||
"email": "integration@example.com",
|
||||
"password": "RegisterPass!9",
|
||||
"student_id": "2023123456",
|
||||
"first_name": "Integration",
|
||||
"last_name": "Tester",
|
||||
"university": self.university_ut.code,
|
||||
"major": self.major_cs.code,
|
||||
"year_of_study": 3,
|
||||
}
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/register", data=json.dumps(payload), content_type="application/json"
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertTrue(User.objects.filter(email=payload["email"]).exists())
|
||||
self.assertTrue(self.mock_signal_verification_task.called)
|
||||
|
||||
def test_register_rejects_short_student_id(self):
|
||||
# Arrange
|
||||
payload = {
|
||||
"username": "short_id",
|
||||
"email": "short@example.com",
|
||||
"password": "RegisterPass!9",
|
||||
"student_id": "123456789", # 9 digits
|
||||
}
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/register", data=json.dumps(payload), content_type="application/json"
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_register_rejects_duplicate_username(self):
|
||||
# Arrange
|
||||
existing = self._create_user(username="duplicate")
|
||||
payload = {
|
||||
"username": existing.username,
|
||||
"email": "someone@example.com",
|
||||
"password": "RegisterPass!9",
|
||||
}
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/register", data=json.dumps(payload), content_type="application/json"
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_register_rejects_duplicate_email(self):
|
||||
# Arrange
|
||||
existing = self._create_user(email="duplicate@example.com")
|
||||
payload = {
|
||||
"username": "newuser",
|
||||
"email": existing.email,
|
||||
"password": "RegisterPass!9",
|
||||
}
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/register", data=json.dumps(payload), content_type="application/json"
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_register_rejects_duplicate_student_id_in_same_university(self):
|
||||
# Arrange
|
||||
student_id = "2023012345"
|
||||
self._create_user(student_id=student_id, university=self.university_gilan)
|
||||
payload = {
|
||||
"username": "dupstudent",
|
||||
"email": "dupstudent@example.com",
|
||||
"password": "RegisterPass!9",
|
||||
"student_id": student_id,
|
||||
"university": self.university_gilan.code,
|
||||
}
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/register", data=json.dumps(payload), content_type="application/json"
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
# Login & Refresh ------------------------------------------------------
|
||||
|
||||
def test_login_returns_tokens_for_verified_user(self):
|
||||
# Arrange
|
||||
user = self._create_user()
|
||||
user.is_email_verified = True
|
||||
user.save(update_fields=["is_email_verified"])
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/login",
|
||||
data=json.dumps({"email": user.email, "password": self.password}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = response.json()
|
||||
self.assertIn("access_token", body)
|
||||
self.assertIn("refresh_token", body)
|
||||
|
||||
def test_login_rejects_unverified_user(self):
|
||||
# Arrange
|
||||
user = self._create_user()
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/login",
|
||||
data=json.dumps({"email": user.email, "password": self.password}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_login_rejects_inactive_user(self):
|
||||
# Arrange
|
||||
user = self._create_user(is_email_verified=True, is_active=False)
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/login",
|
||||
data=json.dumps({"email": user.email, "password": self.password}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_refresh_returns_tokens(self):
|
||||
# Arrange
|
||||
user = self._create_user(is_email_verified=True)
|
||||
tokens = self._login_and_get_tokens(user)
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/refresh",
|
||||
data=json.dumps({"refresh_token": tokens["refresh_token"]}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
refreshed = response.json()
|
||||
self.assertIn("access_token", refreshed)
|
||||
self.assertIn("refresh_token", refreshed)
|
||||
|
||||
def test_refresh_rejects_non_refresh_token(self):
|
||||
# Arrange
|
||||
user = self._create_user(is_email_verified=True)
|
||||
tokens = self._login_and_get_tokens(user)
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/refresh",
|
||||
data=json.dumps({"refresh_token": tokens["access_token"]}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_refresh_rejects_missing_user_id(self):
|
||||
# Arrange
|
||||
token = self._refresh_token_value()
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/refresh",
|
||||
data=json.dumps({"refresh_token": token}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_refresh_rejects_unverified_user(self):
|
||||
# Arrange
|
||||
user = self._create_user()
|
||||
token = self._refresh_token_value(user=user)
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/refresh",
|
||||
data=json.dumps({"refresh_token": token}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_refresh_rejects_inactive_user(self):
|
||||
# Arrange
|
||||
user = self._create_user(is_email_verified=True, is_active=False)
|
||||
token = self._refresh_token_value(user=user)
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/refresh",
|
||||
data=json.dumps({"refresh_token": token}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_refresh_rejects_expired_token(self):
|
||||
# Arrange
|
||||
user = self._create_user(is_email_verified=True)
|
||||
token = self._refresh_token_value(
|
||||
user=user,
|
||||
exp=timezone.now() - timedelta(minutes=1),
|
||||
)
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/refresh",
|
||||
data=json.dumps({"refresh_token": token}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_refresh_rejects_invalid_token_string(self):
|
||||
# Arrange
|
||||
token = "not-a-valid-token"
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/refresh",
|
||||
data=json.dumps({"refresh_token": token}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
# Email verification ---------------------------------------------------
|
||||
|
||||
def test_verify_email_marks_user_verified(self):
|
||||
# Arrange
|
||||
user = self._create_user()
|
||||
token = str(user.email_verification_token)
|
||||
|
||||
# Act
|
||||
response = self.client.get(f"/api/auth/verify-email/{token}")
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
user.refresh_from_db()
|
||||
self.assertTrue(user.is_email_verified)
|
||||
|
||||
def test_verify_email_rejects_unknown_token(self):
|
||||
# Arrange
|
||||
token = uuid.uuid4()
|
||||
|
||||
# Act
|
||||
response = self.client.get(f"/api/auth/verify-email/{token}")
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_resend_verification_rejects_unknown_email(self):
|
||||
# Arrange
|
||||
payload = {"email": "missing@example.com"}
|
||||
|
||||
# Act
|
||||
response = self.client.post(f"/api/auth/resend-verification?email={payload['email']}")
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# Profiles -------------------------------------------------------------
|
||||
|
||||
def test_get_profile_returns_schema_fields(self):
|
||||
# Arrange
|
||||
user = self._create_user(major=self.major_cs, university=self.university_gilan)
|
||||
user.is_email_verified = True
|
||||
user.save(update_fields=["is_email_verified"])
|
||||
tokens = self._login_and_get_tokens(user)
|
||||
|
||||
# Act
|
||||
response = self.client.get("/api/auth/profile", **self._auth_headers(tokens["access_token"]))
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
profile = response.json()
|
||||
self.assertEqual(profile["major"], user.get_major_display())
|
||||
self.assertEqual(profile["university"], user.get_university_display())
|
||||
|
||||
def test_get_profile_requires_authentication(self):
|
||||
# Arrange
|
||||
# No token supplied.
|
||||
|
||||
# Act
|
||||
response = self.client.get("/api/auth/profile")
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_update_profile_persists_changes(self):
|
||||
# Arrange
|
||||
user = self._create_user(is_email_verified=True)
|
||||
tokens = self._login_and_get_tokens(user)
|
||||
payload = {"bio": "Updated bio", "year_of_study": 4}
|
||||
|
||||
# Act
|
||||
response = self.client.put(
|
||||
"/api/auth/profile",
|
||||
data=json.dumps(payload),
|
||||
content_type="application/json",
|
||||
**self._auth_headers(tokens["access_token"]),
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
user.refresh_from_db()
|
||||
self.assertEqual(user.bio, payload["bio"])
|
||||
self.assertEqual(user.year_of_study, payload["year_of_study"])
|
||||
|
||||
@override_settings(MEDIA_URL="/media/", MEDIA_ROOT=tempfile.gettempdir())
|
||||
def test_upload_profile_picture_succeeds(self):
|
||||
# Arrange
|
||||
user = self._create_user(is_email_verified=True)
|
||||
tokens = self._login_and_get_tokens(user)
|
||||
image = SimpleUploadedFile(
|
||||
"avatar.png", b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR", content_type="image/png"
|
||||
)
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/profile/picture", {"file": image}, **self._auth_headers(tokens["access_token"])
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
profile = self.client.get(
|
||||
"/api/auth/profile", **self._auth_headers(tokens["access_token"])
|
||||
).json()
|
||||
self.assertIn("profile_pictures", profile["profile_picture"])
|
||||
|
||||
def test_upload_profile_picture_requires_file(self):
|
||||
# Arrange
|
||||
user = self._create_user(is_email_verified=True)
|
||||
tokens = self._login_and_get_tokens(user)
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/profile/picture", **self._auth_headers(tokens["access_token"])
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_upload_profile_picture_rejects_invalid_type(self):
|
||||
# Arrange
|
||||
user = self._create_user(is_email_verified=True)
|
||||
tokens = self._login_and_get_tokens(user)
|
||||
text_file = SimpleUploadedFile("doc.txt", b"text", content_type="text/plain")
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/profile/picture",
|
||||
{"file": text_file},
|
||||
**self._auth_headers(tokens["access_token"]),
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_upload_profile_picture_rejects_large_files(self):
|
||||
# Arrange
|
||||
user = self._create_user(is_email_verified=True)
|
||||
tokens = self._login_and_get_tokens(user)
|
||||
large_content = b"x" * (5 * 1024 * 1024 + 1)
|
||||
large_file = SimpleUploadedFile("large.png", large_content, content_type="image/png")
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/profile/picture",
|
||||
{"file": large_file},
|
||||
**self._auth_headers(tokens["access_token"]),
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_delete_profile_picture_removes_file(self):
|
||||
# Arrange
|
||||
temp_media = tempfile.mkdtemp()
|
||||
self.addCleanup(lambda: shutil.rmtree(temp_media, ignore_errors=True))
|
||||
user = self._create_user(is_email_verified=True)
|
||||
tokens = self._login_and_get_tokens(user)
|
||||
with override_settings(MEDIA_ROOT=temp_media, MEDIA_URL="/media/"):
|
||||
image = SimpleUploadedFile("avatar.png", b"data", content_type="image/png")
|
||||
self.client.post(
|
||||
"/api/auth/profile/picture",
|
||||
{"file": image},
|
||||
**self._auth_headers(tokens["access_token"]),
|
||||
)
|
||||
|
||||
# Act
|
||||
response = self.client.delete(
|
||||
"/api/auth/profile/picture", **self._auth_headers(tokens["access_token"])
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
user.refresh_from_db()
|
||||
self.assertFalse(bool(user.profile_picture))
|
||||
|
||||
# Password reset ------------------------------------------------------
|
||||
|
||||
def test_request_password_reset_enqueues_email(self):
|
||||
# Arrange
|
||||
user = self._create_user()
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/request-password-reset",
|
||||
data=json.dumps({"email": user.email}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
user.refresh_from_db()
|
||||
self.assertIsNotNone(user.password_reset_token)
|
||||
self.mock_password_reset_task.assert_called_once()
|
||||
|
||||
def test_request_password_reset_unknown_email_returns_error(self):
|
||||
# Arrange
|
||||
payload = {"email": "missing@example.com"}
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/request-password-reset",
|
||||
data=json.dumps(payload),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_reset_password_confirm_updates_credentials(self):
|
||||
# Arrange
|
||||
user = self._create_user()
|
||||
user.set_password_reset_token()
|
||||
payload = {"token": str(user.password_reset_token), "new_password": "BrandNewPass!9"}
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/reset-password-confirm",
|
||||
data=json.dumps(payload),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
user.refresh_from_db()
|
||||
self.assertIsNone(user.password_reset_token)
|
||||
self.assertTrue(user.check_password(payload["new_password"]))
|
||||
|
||||
def test_reset_password_confirm_rejects_expired_token(self):
|
||||
# Arrange
|
||||
user = self._create_user()
|
||||
user.set_password_reset_token()
|
||||
user.password_reset_token_expires_at = timezone.now() - timedelta(minutes=1)
|
||||
user.save(update_fields=["password_reset_token_expires_at"])
|
||||
payload = {"token": str(user.password_reset_token), "new_password": "New!!!Pass"}
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/reset-password-confirm",
|
||||
data=json.dumps(payload),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_reset_password_confirm_rejects_unknown_token(self):
|
||||
# Arrange
|
||||
payload = {"token": str(uuid.uuid4()), "new_password": "AnotherPass!9"}
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/reset-password-confirm",
|
||||
data=json.dumps(payload),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
# Admin utilities -----------------------------------------------------
|
||||
|
||||
def test_list_deleted_users_requires_privileged_user(self):
|
||||
# Arrange
|
||||
user = self._create_user(is_email_verified=True)
|
||||
tokens = self._login_and_get_tokens(user)
|
||||
|
||||
# Act
|
||||
response = self.client.get(
|
||||
"/api/auth/users/deleted", **self._auth_headers(tokens["access_token"])
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_list_deleted_users_returns_payload_for_staff(self):
|
||||
# Arrange
|
||||
deleted = self._create_user(is_deleted=True, deleted_at=timezone.now())
|
||||
staff = self._create_user(is_email_verified=True, is_staff=True)
|
||||
tokens = self._login_and_get_tokens(staff)
|
||||
|
||||
# Act
|
||||
response = self.client.get(
|
||||
"/api/auth/users/deleted", **self._auth_headers(tokens["access_token"])
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
payload = response.json()
|
||||
self.assertTrue(any(item["id"] == deleted.id for item in payload))
|
||||
|
||||
def test_restore_user_requires_privileged_user(self):
|
||||
# Arrange
|
||||
target = self._create_user(is_deleted=True, deleted_at=timezone.now())
|
||||
user = self._create_user(is_email_verified=True)
|
||||
tokens = self._login_and_get_tokens(user)
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
f"/api/auth/users/{target.id}/restore", **self._auth_headers(tokens["access_token"])
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_restore_user_restores_record_for_staff(self):
|
||||
# Arrange
|
||||
target = self._create_user(is_deleted=True, deleted_at=timezone.now())
|
||||
staff = self._create_user(is_email_verified=True, is_staff=True)
|
||||
tokens = self._login_and_get_tokens(staff)
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
f"/api/auth/users/{target.id}/restore", **self._auth_headers(tokens["access_token"])
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
target.refresh_from_db()
|
||||
self.assertFalse(target.is_deleted)
|
||||
|
||||
def test_restore_user_missing_returns_error(self):
|
||||
# Arrange
|
||||
staff = self._create_user(is_email_verified=True, is_staff=True)
|
||||
tokens = self._login_and_get_tokens(staff)
|
||||
|
||||
# Act
|
||||
response = self.client.post(
|
||||
"/api/auth/users/999/restore", **self._auth_headers(tokens["access_token"])
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
# Username checks ------------------------------------------------------
|
||||
|
||||
def test_check_username_reports_existing(self):
|
||||
# Arrange
|
||||
user = self._create_user()
|
||||
|
||||
# Act
|
||||
response = self.client.get("/api/auth/check-username", {"username": user.username})
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.json()["exists"])
|
||||
|
||||
def test_check_username_reports_availability(self):
|
||||
# Arrange
|
||||
username = "available_user"
|
||||
|
||||
# Act
|
||||
response = self.client.get("/api/auth/check-username", {"username": username})
|
||||
|
||||
# Assert
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(response.json()["exists"])
|
||||
0
apps/users/tests/unit/__init__.py
Normal file
0
apps/users/tests/unit/__init__.py
Normal file
400
apps/users/tests/unit/test_users.py
Normal file
400
apps/users/tests/unit/test_users.py
Normal file
@@ -0,0 +1,400 @@
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from unittest import mock
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.test import SimpleTestCase, TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from import_export.widgets import BooleanWidget
|
||||
|
||||
from apps.users.models import User, Major, University
|
||||
from apps.users.resources import UserResource
|
||||
from apps.users.signals import send_verification_email_on_registration
|
||||
from apps.users.tasks import (
|
||||
send_email_verified_success,
|
||||
send_password_reset_email,
|
||||
send_verification_email,
|
||||
)
|
||||
|
||||
|
||||
class UserFactoryMixin:
|
||||
def _ensure_reference_objects(self):
|
||||
if not hasattr(self, "_default_major"):
|
||||
self._default_major, _ = Major.objects.get_or_create(
|
||||
code="CS",
|
||||
defaults={"name": "Computer Science"},
|
||||
)
|
||||
self._default_university, _ = University.objects.get_or_create(
|
||||
code="UT",
|
||||
defaults={"name": "University of Tehran"},
|
||||
)
|
||||
|
||||
def _resolve_major(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, Major):
|
||||
return value
|
||||
obj, _ = Major.objects.get_or_create(code=value, defaults={"name": value})
|
||||
return obj
|
||||
|
||||
def _resolve_university(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, University):
|
||||
return value
|
||||
obj, _ = University.objects.get_or_create(code=value, defaults={"name": value})
|
||||
return obj
|
||||
|
||||
def create_user(self, **extra_fields):
|
||||
self._ensure_reference_objects()
|
||||
unique = uuid.uuid4().hex
|
||||
data = {
|
||||
"email": f"user_{unique}@example.com",
|
||||
"username": f"user_{unique[:10]}",
|
||||
"first_name": "Test",
|
||||
"last_name": "User",
|
||||
}
|
||||
password = extra_fields.pop("password", "StrongPass!123")
|
||||
major = extra_fields.pop("major", self._default_major)
|
||||
university = extra_fields.pop("university", self._default_university)
|
||||
if isinstance(major, str):
|
||||
major = self._resolve_major(major)
|
||||
if isinstance(university, str):
|
||||
university = self._resolve_university(university)
|
||||
data.update(extra_fields)
|
||||
data.setdefault("major", major)
|
||||
data.setdefault("university", university)
|
||||
return User.objects.create_user(password=password, **data)
|
||||
|
||||
|
||||
class UserModelTests(UserFactoryMixin, TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
patcher = mock.patch("apps.users.signals.send_verification_email.delay")
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def test_str_returns_full_name_with_email(self):
|
||||
# Arrange
|
||||
user = self.create_user(first_name="Ada", last_name="Lovelace")
|
||||
|
||||
# Act
|
||||
result = str(user)
|
||||
|
||||
# Assert
|
||||
expected = f"{user.get_full_name()} ({user.email})"
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_get_full_name_handles_missing_names(self):
|
||||
# Arrange
|
||||
user = self.create_user(first_name="Grace", last_name="")
|
||||
|
||||
# Act
|
||||
result = user.get_full_name()
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, "Grace")
|
||||
|
||||
def test_regenerate_verification_token_generates_new_value(self):
|
||||
# Arrange
|
||||
user = self.create_user()
|
||||
original_token = user.email_verification_token
|
||||
|
||||
# Act
|
||||
user.regenerate_verification_token()
|
||||
|
||||
# Assert
|
||||
self.assertNotEqual(user.email_verification_token, original_token)
|
||||
|
||||
def test_set_password_reset_token_assigns_future_expiry(self):
|
||||
# Arrange
|
||||
user = self.create_user()
|
||||
frozen = timezone.now()
|
||||
|
||||
# Act
|
||||
with mock.patch("apps.users.models.timezone.now", return_value=frozen):
|
||||
user.set_password_reset_token()
|
||||
|
||||
# Assert
|
||||
self.assertIsNotNone(user.password_reset_token)
|
||||
self.assertEqual(
|
||||
user.password_reset_token_expires_at,
|
||||
frozen + timedelta(hours=1),
|
||||
)
|
||||
|
||||
def test_save_triggers_verified_task_on_state_change(self):
|
||||
# Arrange
|
||||
user = self.create_user()
|
||||
|
||||
# Act
|
||||
with mock.patch("apps.users.tasks.send_email_verified_success.delay") as mock_delay:
|
||||
user.is_email_verified = True
|
||||
user.save()
|
||||
|
||||
# Assert
|
||||
mock_delay.assert_called_once_with(user.id)
|
||||
|
||||
def test_save_skips_task_when_already_verified(self):
|
||||
# Arrange
|
||||
user = self.create_user(is_email_verified=True)
|
||||
|
||||
# Act
|
||||
with mock.patch("apps.users.tasks.send_email_verified_success.delay") as mock_delay:
|
||||
user.bio = "Updated bio"
|
||||
user.save()
|
||||
|
||||
# Assert
|
||||
mock_delay.assert_not_called()
|
||||
|
||||
|
||||
class UserSignalTests(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
post_save.disconnect(send_verification_email_on_registration, sender=User)
|
||||
self.addCleanup(
|
||||
post_save.connect,
|
||||
send_verification_email_on_registration,
|
||||
User,
|
||||
False,
|
||||
)
|
||||
|
||||
@override_settings(FRONTEND_ROOT="https://frontend.example/")
|
||||
@mock.patch("apps.users.signals.send_verification_email.delay")
|
||||
@mock.patch("apps.users.signals.uuid.uuid4")
|
||||
def test_signal_sets_username_timestamp_and_dispatches_email(
|
||||
self,
|
||||
mock_uuid,
|
||||
mock_delay,
|
||||
):
|
||||
# Arrange
|
||||
fake_uuid = uuid.UUID("12345678-1234-5678-1234-567812345678")
|
||||
mock_uuid.return_value = fake_uuid
|
||||
fake_now = timezone.now()
|
||||
user = User.objects.create(
|
||||
email="new.user@example.com",
|
||||
username="",
|
||||
password="pass",
|
||||
is_email_verified=False,
|
||||
)
|
||||
|
||||
# Act
|
||||
with mock.patch("apps.users.signals.timezone.now", return_value=fake_now):
|
||||
send_verification_email_on_registration(User, user, created=True)
|
||||
|
||||
# Assert
|
||||
user.refresh_from_db()
|
||||
self.assertEqual(user.username, str(fake_uuid)[:10])
|
||||
self.assertEqual(user.email_verification_sent_at, fake_now)
|
||||
expected_url = (
|
||||
f"https://frontend.example/verify-email/{user.email_verification_token}"
|
||||
)
|
||||
mock_delay.assert_called_once_with(user.id, expected_url)
|
||||
|
||||
@override_settings(FRONTEND_ROOT="https://frontend.example/")
|
||||
@mock.patch("apps.users.signals.send_verification_email.delay")
|
||||
def test_signal_preserves_existing_username(self, mock_delay):
|
||||
# Arrange
|
||||
fake_now = timezone.now()
|
||||
user = User.objects.create(
|
||||
email="existing@example.com",
|
||||
username="existing_name",
|
||||
password="pass",
|
||||
is_email_verified=False,
|
||||
)
|
||||
|
||||
# Act
|
||||
with mock.patch("apps.users.signals.timezone.now", return_value=fake_now):
|
||||
send_verification_email_on_registration(User, user, created=True)
|
||||
|
||||
# Assert
|
||||
user.refresh_from_db()
|
||||
self.assertEqual(user.username, "existing_name")
|
||||
self.assertEqual(user.email_verification_sent_at, fake_now)
|
||||
mock_delay.assert_called_once()
|
||||
|
||||
@mock.patch("apps.users.signals.send_verification_email.delay")
|
||||
def test_signal_skips_when_user_already_verified(self, mock_delay):
|
||||
# Arrange
|
||||
user = User.objects.create(
|
||||
email="verified@example.com",
|
||||
username="verified_user",
|
||||
password="pass",
|
||||
is_email_verified=True,
|
||||
)
|
||||
|
||||
# Act
|
||||
send_verification_email_on_registration(User, user, created=True)
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(user.email_verification_sent_at)
|
||||
mock_delay.assert_not_called()
|
||||
|
||||
@mock.patch("apps.users.signals.send_verification_email.delay")
|
||||
def test_signal_skips_when_email_missing(self, mock_delay):
|
||||
# Arrange
|
||||
user = User.objects.create(
|
||||
email="",
|
||||
username="no_email",
|
||||
password="pass",
|
||||
is_email_verified=False,
|
||||
)
|
||||
|
||||
# Act
|
||||
send_verification_email_on_registration(User, user, created=True)
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(user.email_verification_sent_at)
|
||||
mock_delay.assert_not_called()
|
||||
|
||||
@mock.patch("apps.users.signals.send_verification_email.delay")
|
||||
def test_signal_ignores_updates_to_existing_users(self, mock_delay):
|
||||
# Arrange
|
||||
user = User.objects.create(
|
||||
email="existing-update@example.com",
|
||||
username="existing_update",
|
||||
password="pass",
|
||||
is_email_verified=False,
|
||||
)
|
||||
|
||||
# Act
|
||||
send_verification_email_on_registration(User, user, created=False)
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(user.email_verification_sent_at)
|
||||
mock_delay.assert_not_called()
|
||||
|
||||
|
||||
class UserTaskTests(UserFactoryMixin, TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
patcher = mock.patch("apps.users.signals.send_verification_email.delay")
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
@override_settings(DEFAULT_FROM_EMAIL="no-reply@example.com")
|
||||
@mock.patch("apps.users.tasks.send_mail")
|
||||
@mock.patch("apps.users.tasks.render_to_string", return_value="<p>Hi</p>")
|
||||
def test_send_verification_email_task_sends_expected_payload(
|
||||
self,
|
||||
mock_render,
|
||||
mock_send_mail,
|
||||
):
|
||||
# Arrange
|
||||
user = self.create_user()
|
||||
verification_url = "https://example.com/verify"
|
||||
|
||||
# Act
|
||||
result = send_verification_email.run(user.id, verification_url)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, f"Verification email sent to {user.email}")
|
||||
mock_render.assert_called_once_with(
|
||||
"emails/verification_email.html",
|
||||
{"user": user, "verification_url": verification_url},
|
||||
)
|
||||
kwargs = mock_send_mail.call_args.kwargs
|
||||
self.assertEqual(kwargs["recipient_list"], [user.email])
|
||||
self.assertEqual(kwargs["from_email"], "no-reply@example.com")
|
||||
self.assertEqual(kwargs["message"], "Hi")
|
||||
|
||||
@override_settings(DEFAULT_FROM_EMAIL="support@example.com")
|
||||
@mock.patch("apps.users.tasks.send_mail")
|
||||
@mock.patch("apps.users.tasks.render_to_string", return_value="<p>Reset</p>")
|
||||
def test_send_password_reset_email_task_uses_reset_template(
|
||||
self,
|
||||
mock_render,
|
||||
mock_send_mail,
|
||||
):
|
||||
# Arrange
|
||||
user = self.create_user()
|
||||
reset_url = "https://example.com/reset"
|
||||
|
||||
# Act
|
||||
result = send_password_reset_email.run(user.id, reset_url)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, f"Password reset email sent to {user.email}")
|
||||
mock_render.assert_called_once_with(
|
||||
"emails/password_reset_email.html",
|
||||
{"user": user, "reset_url": reset_url},
|
||||
)
|
||||
kwargs = mock_send_mail.call_args.kwargs
|
||||
self.assertEqual(kwargs["recipient_list"], [user.email])
|
||||
self.assertEqual(kwargs["from_email"], "support@example.com")
|
||||
self.assertEqual(kwargs["message"], "Reset")
|
||||
|
||||
@override_settings(
|
||||
DEFAULT_FROM_EMAIL="success@example.com",
|
||||
FRONTEND_ROOT="https://frontend.example/",
|
||||
)
|
||||
@mock.patch("apps.users.tasks.send_mail")
|
||||
@mock.patch("apps.users.tasks.render_to_string", return_value="<p>Success</p>")
|
||||
def test_send_email_verified_success_task_renders_success_template(
|
||||
self,
|
||||
mock_render,
|
||||
mock_send_mail,
|
||||
):
|
||||
# Arrange
|
||||
user = self.create_user()
|
||||
|
||||
# Act
|
||||
result = send_email_verified_success.run(user.id)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, f"verified success email sent to {user.email}")
|
||||
mock_render.assert_called_once_with(
|
||||
"emails/verification_success.html",
|
||||
{"user": user, "home_url": "https://frontend.example/"},
|
||||
)
|
||||
kwargs = mock_send_mail.call_args.kwargs
|
||||
self.assertEqual(kwargs["recipient_list"], [user.email])
|
||||
self.assertEqual(kwargs["from_email"], "success@example.com")
|
||||
self.assertEqual(kwargs["message"], "Success")
|
||||
|
||||
def test_send_verification_email_task_retries_on_lookup_error(self):
|
||||
# Arrange
|
||||
retry_patch = mock.patch.object(
|
||||
send_verification_email,
|
||||
"retry",
|
||||
side_effect=RuntimeError("retry"),
|
||||
)
|
||||
|
||||
# Act / Assert
|
||||
with mock.patch(
|
||||
"apps.users.tasks.User.objects.get",
|
||||
side_effect=ValueError("missing"),
|
||||
), retry_patch as mock_retry:
|
||||
with self.assertRaises(RuntimeError):
|
||||
send_verification_email.run(999, "https://example.com/verify")
|
||||
|
||||
self.assertEqual(mock_retry.call_args.kwargs.get("countdown"), 60)
|
||||
self.assertIsInstance(mock_retry.call_args.kwargs.get("exc"), ValueError)
|
||||
|
||||
|
||||
class UserResourceTests(SimpleTestCase):
|
||||
def test_boolean_fields_use_boolean_widget(self):
|
||||
# Arrange
|
||||
resource = UserResource()
|
||||
|
||||
# Act
|
||||
widgets = [
|
||||
resource.fields["is_staff"].widget,
|
||||
resource.fields["is_superuser"].widget,
|
||||
resource.fields["is_email_verified"].widget,
|
||||
]
|
||||
|
||||
# Assert
|
||||
for widget in widgets:
|
||||
self.assertIsInstance(widget, BooleanWidget)
|
||||
|
||||
def test_field_order_matches_meta_definition(self):
|
||||
# Arrange
|
||||
resource = UserResource()
|
||||
|
||||
# Act
|
||||
field_names = tuple(resource.fields.keys())
|
||||
|
||||
# Assert
|
||||
self.assertEqual(resource._meta.export_order, resource._meta.fields)
|
||||
self.assertSetEqual(set(field_names), set(resource._meta.fields))
|
||||
Reference in New Issue
Block a user