initial commit
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-05-19 20:53:08 +03:30
commit 88b793ed9f
169 changed files with 16763 additions and 0 deletions

122
apps/users/admin.py Normal file
View 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')

View File

15
apps/users/api/meta.py Normal file
View 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
View 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
View 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
View 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

View 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"}}
]

View 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
}
}
]

View 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()),
],
),
]

View 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),
),
]

File diff suppressed because one or more lines are too long

View 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",
),
),
]

View 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),
]

View 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",
),
]

View File

112
apps/users/models.py Normal file
View 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
View 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
View 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
View 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)

View File

View File

View 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"])

View File

View 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))