Compare commits
3 Commits
22e08a099c
...
d18fdb1454
| Author | SHA1 | Date | |
|---|---|---|---|
| d18fdb1454 | |||
| 5500badc6a | |||
| 2a0fa22be6 |
@@ -24,9 +24,10 @@ from apps.projects.services.access import (
|
|||||||
build_project_access_items,
|
build_project_access_items,
|
||||||
ensure_workspace_project_access,
|
ensure_workspace_project_access,
|
||||||
filter_projects_for_user,
|
filter_projects_for_user,
|
||||||
get_access_managed_membership,
|
get_project_access_target_membership,
|
||||||
grant_project_accesses,
|
grant_project_accesses,
|
||||||
revoke_project_accesses,
|
revoke_project_accesses,
|
||||||
|
user_has_project_access,
|
||||||
)
|
)
|
||||||
from apps.projects.services.rates import get_current_project_user_rate, remove_project_user_rate, upsert_project_user_rate
|
from apps.projects.services.rates import get_current_project_user_rate, remove_project_user_rate, upsert_project_user_rate
|
||||||
from apps.projects.services.projects import (
|
from apps.projects.services.projects import (
|
||||||
@@ -182,7 +183,7 @@ class ProjectViewSet(ModelViewSet):
|
|||||||
is_deleted=False,
|
is_deleted=False,
|
||||||
)
|
)
|
||||||
ensure_workspace_project_access(request.user, workspace)
|
ensure_workspace_project_access(request.user, workspace)
|
||||||
membership = get_access_managed_membership(workspace, str(serializer.validated_data["user"]))
|
membership = get_project_access_target_membership(workspace, str(serializer.validated_data["user"]))
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@@ -207,7 +208,7 @@ class ProjectViewSet(ModelViewSet):
|
|||||||
id=serializer.validated_data["workspace"],
|
id=serializer.validated_data["workspace"],
|
||||||
is_deleted=False,
|
is_deleted=False,
|
||||||
)
|
)
|
||||||
membership = get_access_managed_membership(workspace, str(serializer.validated_data["user"]))
|
membership = get_project_access_target_membership(workspace, str(serializer.validated_data["user"]))
|
||||||
changed = grant_project_accesses(
|
changed = grant_project_accesses(
|
||||||
actor=request.user,
|
actor=request.user,
|
||||||
workspace=workspace,
|
workspace=workspace,
|
||||||
@@ -226,7 +227,7 @@ class ProjectViewSet(ModelViewSet):
|
|||||||
id=serializer.validated_data["workspace"],
|
id=serializer.validated_data["workspace"],
|
||||||
is_deleted=False,
|
is_deleted=False,
|
||||||
)
|
)
|
||||||
membership = get_access_managed_membership(workspace, str(serializer.validated_data["user"]))
|
membership = get_project_access_target_membership(workspace, str(serializer.validated_data["user"]))
|
||||||
changed = revoke_project_accesses(
|
changed = revoke_project_accesses(
|
||||||
actor=request.user,
|
actor=request.user,
|
||||||
workspace=workspace,
|
workspace=workspace,
|
||||||
@@ -246,7 +247,7 @@ class ProjectViewSet(ModelViewSet):
|
|||||||
is_deleted=False,
|
is_deleted=False,
|
||||||
)
|
)
|
||||||
ensure_workspace_project_access(request.user, workspace)
|
ensure_workspace_project_access(request.user, workspace)
|
||||||
membership = get_access_managed_membership(workspace, str(serializer.validated_data["user"]))
|
membership = get_project_access_target_membership(workspace, str(serializer.validated_data["user"]))
|
||||||
project = get_object_or_404(
|
project = get_object_or_404(
|
||||||
Project,
|
Project,
|
||||||
id=serializer.validated_data["project"],
|
id=serializer.validated_data["project"],
|
||||||
@@ -254,7 +255,7 @@ class ProjectViewSet(ModelViewSet):
|
|||||||
is_deleted=False,
|
is_deleted=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
has_access = membership.user.project_accesses.filter(project=project).exists()
|
has_access = user_has_project_access(membership.user, project)
|
||||||
if not has_access:
|
if not has_access:
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "Grant project access before setting a project-specific rate."},
|
{"detail": "Grant project access before setting a project-specific rate."},
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ def ensure_workspace_project_access(user, workspace: Workspace) -> None:
|
|||||||
raise PermissionDenied("You do not have permission to manage project access in this workspace.")
|
raise PermissionDenied("You do not have permission to manage project access in this workspace.")
|
||||||
|
|
||||||
|
|
||||||
def get_access_managed_membership(workspace: Workspace, user_id: str) -> WorkspaceMembership:
|
def get_project_access_target_membership(workspace: Workspace, user_id: str) -> WorkspaceMembership:
|
||||||
membership = WorkspaceMembership.objects.filter(
|
membership = WorkspaceMembership.objects.filter(
|
||||||
workspace=workspace,
|
workspace=workspace,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -75,8 +75,6 @@ def get_access_managed_membership(workspace: Workspace, user_id: str) -> Workspa
|
|||||||
).select_related("user").first()
|
).select_related("user").first()
|
||||||
if not membership:
|
if not membership:
|
||||||
raise ValidationError({"user": "Selected user is not an active member of this workspace."})
|
raise ValidationError({"user": "Selected user is not an active member of this workspace."})
|
||||||
if membership.role not in PROJECT_ACCESS_MANAGED_ROLES:
|
|
||||||
raise ValidationError({"user": "Owners and admins have implicit access to all projects."})
|
|
||||||
return membership
|
return membership
|
||||||
|
|
||||||
|
|
||||||
@@ -146,7 +144,7 @@ def build_project_access_items(*, workspace: Workspace, target_user) -> list[dic
|
|||||||
return [
|
return [
|
||||||
build_project_access_item(
|
build_project_access_item(
|
||||||
project=project,
|
project=project,
|
||||||
has_access=str(project.id) in explicit_access_ids,
|
has_access=user_has_project_access(target_user, project) if user_has_implicit_project_access(target_user, workspace) else str(project.id) in explicit_access_ids,
|
||||||
workspace_rate=workspace_rate,
|
workspace_rate=workspace_rate,
|
||||||
project_rate=project_rates.get(str(project.id)),
|
project_rate=project_rates.get(str(project.id)),
|
||||||
)
|
)
|
||||||
@@ -156,7 +154,9 @@ def build_project_access_items(*, workspace: Workspace, target_user) -> list[dic
|
|||||||
|
|
||||||
def grant_project_accesses(*, actor, workspace: Workspace, target_user, project_ids: list[str]) -> int:
|
def grant_project_accesses(*, actor, workspace: Workspace, target_user, project_ids: list[str]) -> int:
|
||||||
ensure_workspace_project_access(actor, workspace)
|
ensure_workspace_project_access(actor, workspace)
|
||||||
get_access_managed_membership(workspace, str(target_user.id))
|
membership = get_project_access_target_membership(workspace, str(target_user.id))
|
||||||
|
if membership.role not in PROJECT_ACCESS_MANAGED_ROLES:
|
||||||
|
raise ValidationError({"user": "Owners and admins already have access to all projects."})
|
||||||
|
|
||||||
projects = list(Project.objects.filter(workspace=workspace, id__in=project_ids, is_deleted=False))
|
projects = list(Project.objects.filter(workspace=workspace, id__in=project_ids, is_deleted=False))
|
||||||
if len(projects) != len(set(project_ids)):
|
if len(projects) != len(set(project_ids)):
|
||||||
@@ -175,7 +175,9 @@ def grant_project_accesses(*, actor, workspace: Workspace, target_user, project_
|
|||||||
|
|
||||||
def revoke_project_accesses(*, actor, workspace: Workspace, target_user, project_ids: list[str]) -> int:
|
def revoke_project_accesses(*, actor, workspace: Workspace, target_user, project_ids: list[str]) -> int:
|
||||||
ensure_workspace_project_access(actor, workspace)
|
ensure_workspace_project_access(actor, workspace)
|
||||||
get_access_managed_membership(workspace, str(target_user.id))
|
membership = get_project_access_target_membership(workspace, str(target_user.id))
|
||||||
|
if membership.role not in PROJECT_ACCESS_MANAGED_ROLES:
|
||||||
|
raise ValidationError({"user": "Owners and admins always keep project access."})
|
||||||
|
|
||||||
accesses = list(
|
accesses = list(
|
||||||
ProjectAccess.objects.filter(
|
ProjectAccess.objects.filter(
|
||||||
|
|||||||
@@ -191,3 +191,29 @@ class ProjectViewTests(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertIn("Grant project access", response.data["detail"])
|
self.assertIn("Grant project access", response.data["detail"])
|
||||||
|
|
||||||
|
def test_owner_access_state_marks_all_projects_as_accessible_and_allows_project_rate_override(self):
|
||||||
|
self.client.force_authenticate(user=self.owner)
|
||||||
|
|
||||||
|
access_response = self.client.get(
|
||||||
|
"/api/projects/access/",
|
||||||
|
{"workspace": str(self.workspace.id), "user": str(self.owner.id)},
|
||||||
|
)
|
||||||
|
self.assertEqual(access_response.status_code, 200)
|
||||||
|
self.assertTrue(all(item["has_access"] for item in access_response.data["items"]))
|
||||||
|
|
||||||
|
save_response = self.client.post(
|
||||||
|
"/api/projects/access/rate/",
|
||||||
|
{
|
||||||
|
"workspace": str(self.workspace.id),
|
||||||
|
"user": str(self.owner.id),
|
||||||
|
"project": str(self.first_project.id),
|
||||||
|
"hourly_rate": "60.00",
|
||||||
|
"currency": "USD",
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(save_response.status_code, 200)
|
||||||
|
self.assertEqual(save_response.data["item"]["project_rate"]["hourly_rate"], "60.00")
|
||||||
|
self.assertTrue(ProjectUserRate.objects.filter(project=self.first_project, user=self.owner, is_deleted=False).exists())
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ UNCATEGORIZED_LABELS = {
|
|||||||
"tags": "No tag",
|
"tags": "No tag",
|
||||||
},
|
},
|
||||||
"fa": {
|
"fa": {
|
||||||
"clients": "\u0628\u062f\u0648\u0646 \u0645\u0634\u062a\u0631\u06cc",
|
"clients": "بدون مشتری",
|
||||||
"projects": "\u0628\u062f\u0648\u0646 \u067e\u0631\u0648\u0698\u0647",
|
"projects": "بدون پروژه",
|
||||||
"tags": "\u0628\u062f\u0648\u0646 \u062a\u06af",
|
"tags": "بدون تگ",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import jdatetime
|
|||||||
from arabic_reshaper import reshape
|
from arabic_reshaper import reshape
|
||||||
from bidi.algorithm import get_display
|
from bidi.algorithm import get_display
|
||||||
|
|
||||||
PERSIAN_DIGITS = str.maketrans("0123456789", "\u06f0\u06f1\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9")
|
PERSIAN_DIGITS = str.maketrans("0123456789", "۰۱۲۳۴۵۶۷۸۹")
|
||||||
ARABIC_RANGES = (
|
ARABIC_RANGES = (
|
||||||
(0x0600, 0x06FF),
|
(0x0600, 0x06FF),
|
||||||
(0x0750, 0x077F),
|
(0x0750, 0x077F),
|
||||||
@@ -70,52 +70,52 @@ TRANSLATIONS = {
|
|||||||
"uncategorized_tag": "No tag",
|
"uncategorized_tag": "No tag",
|
||||||
},
|
},
|
||||||
"fa": {
|
"fa": {
|
||||||
"report_title": "\u06af\u0632\u0627\u0631\u0634 \u0641\u0636\u0627\u06cc \u06a9\u0627\u0631\u06cc",
|
"report_title": "گزارش فضای کاری",
|
||||||
"overall_sheet": "\u06af\u0632\u0627\u0631\u0634 \u06a9\u0644\u06cc",
|
"overall_sheet": "گزارش کلی",
|
||||||
"users_summary_sheet": "\u062e\u0644\u0627\u0635\u0647 \u06a9\u0627\u0631\u0628\u0631\u0627\u0646",
|
"users_summary_sheet": "خلاصه کاربران",
|
||||||
"workspace": "\u0641\u0636\u0627\u06cc \u06a9\u0627\u0631\u06cc",
|
"workspace": "فضای کاری",
|
||||||
"period": "\u0628\u0627\u0632\u0647",
|
"period": "بازه",
|
||||||
"from_date": "\u0627\u0632 \u062a\u0627\u0631\u06cc\u062e",
|
"from_date": "از تاریخ",
|
||||||
"to_date": "\u062a\u0627 \u062a\u0627\u0631\u06cc\u062e",
|
"to_date": "تا تاریخ",
|
||||||
"user": "\u06a9\u0627\u0631\u0628\u0631",
|
"user": "کاربر",
|
||||||
"mobile": "\u0645\u0648\u0628\u0627\u06cc\u0644",
|
"mobile": "موبایل",
|
||||||
"all_users": "\u0647\u0645\u0647 \u06a9\u0627\u0631\u0628\u0631\u0627\u0646",
|
"all_users": "همه کاربران",
|
||||||
"generated_at": "\u062a\u0627\u0631\u06cc\u062e \u062a\u0648\u0644\u06cc\u062f",
|
"generated_at": "تاریخ تولید",
|
||||||
"summary": "\u062e\u0644\u0627\u0635\u0647",
|
"summary": "خلاصه",
|
||||||
"total_hours": "\u06a9\u0644 \u0633\u0627\u0639\u0627\u062a",
|
"total_hours": "کل ساعات",
|
||||||
"billable_hours": "\u0633\u0627\u0639\u0627\u062a \u06a9\u0627\u0631\u06cc",
|
"billable_hours": "ساعات کاری",
|
||||||
"non_billable_hours": "\u0633\u0627\u0639\u0627\u062a \u063a\u06cc\u0631 \u06a9\u0627\u0631\u06cc",
|
"non_billable_hours": "ساعات غیر کاری",
|
||||||
"hourly_rate": "\u0646\u0631\u062e \u0633\u0627\u0639\u062a\u06cc",
|
"hourly_rate": "نرخ ساعتی",
|
||||||
"income": "\u06a9\u0627\u0631\u06a9\u0631\u062f",
|
"income": "کارکرد",
|
||||||
"working_hours": "\u0633\u0627\u0639\u0627\u062a \u06a9\u0627\u0631\u06cc",
|
"working_hours": "ساعات کاری",
|
||||||
"non_working_hours": "\u0633\u0627\u0639\u0627\u062a \u063a\u06cc\u0631\u06a9\u0627\u0631\u06cc",
|
"non_working_hours": "ساعات غیرکاری",
|
||||||
"hourly_rates": "\u0646\u0631\u062e\u200c\u0647\u0627\u06cc \u0633\u0627\u0639\u062a\u06cc",
|
"hourly_rates": "نرخهای ساعتی",
|
||||||
"project_percentages": "\u062f\u0631\u0635\u062f \u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627",
|
"project_percentages": "درصد پروژهها",
|
||||||
"client_percentages": "\u062f\u0631\u0635\u062f \u0645\u0634\u062a\u0631\u06cc\u200c\u0647\u0627",
|
"client_percentages": "درصد مشتریها",
|
||||||
"tag_percentages": "\u062f\u0631\u0635\u062f \u062a\u06af\u200c\u0647\u0627",
|
"tag_percentages": "درصد تگها",
|
||||||
"summary_by_user": "\u062e\u0644\u0627\u0635\u0647 \u06a9\u0627\u0631\u0628\u0631\u0627\u0646",
|
"summary_by_user": "خلاصه کاربران",
|
||||||
"rate_history": "\u062a\u0627\u0631\u06cc\u062e\u0686\u0647 \u0646\u0631\u062e \u0633\u0627\u0639\u062a\u06cc",
|
"rate_history": "تاریخچه نرخ ساعتی",
|
||||||
"from": "\u0627\u0632",
|
"from": "از",
|
||||||
"to": "\u062a\u0627",
|
"to": "تا",
|
||||||
"now": "\u062d\u0627\u0644",
|
"now": "حال",
|
||||||
"project": "\u067e\u0631\u0648\u0698\u0647",
|
"project": "پروژه",
|
||||||
"percentage": "\u062f\u0631\u0635\u062f",
|
"percentage": "درصد",
|
||||||
"hour_percentage": "\u062f\u0631\u0635\u062f \u0633\u0627\u0639\u062a",
|
"hour_percentage": "درصد ساعت",
|
||||||
"income_percentage": "\u062f\u0631\u0635\u062f \u06a9\u0627\u0631\u06a9\u0631\u062f",
|
"income_percentage": "درصد کارکرد",
|
||||||
"multiple_rates": "\u0686\u0646\u062f \u0646\u0631\u062e - \u062c\u0632\u0626\u06cc\u0627\u062a \u062f\u0631 \u06af\u0632\u0627\u0631\u0634 \u06a9\u0627\u0631\u0628\u0631",
|
"multiple_rates": "چند نرخ - جزئیات در گزارش کاربر",
|
||||||
"variable_rate": "\u0646\u0631\u062e \u0645\u062a\u063a\u06cc\u0631",
|
"variable_rate": "نرخ متغیر",
|
||||||
"none": "\u0628\u062f\u0648\u0646 \u0645\u0648\u0631\u062f",
|
"none": "بدون مورد",
|
||||||
"daily_summary": "\u062e\u0644\u0627\u0635\u0647 \u0631\u0648\u0632\u0627\u0646\u0647",
|
"daily_summary": "خلاصه روزانه",
|
||||||
"clients": "\u0645\u0634\u062a\u0631\u06cc\u0627\u0646",
|
"clients": "مشتریان",
|
||||||
"projects": "\u067e\u0631\u0648\u0698\u0647\u200c\u0647\u0627",
|
"projects": "پروژهها",
|
||||||
"tags": "\u062a\u06af\u200c\u0647\u0627",
|
"tags": "تگها",
|
||||||
"date": "\u062a\u0627\u0631\u06cc\u062e",
|
"date": "تاریخ",
|
||||||
"name": "\u0646\u0627\u0645",
|
"name": "نام",
|
||||||
"total": "\u062c\u0645\u0639",
|
"total": "جمع",
|
||||||
"no_data": "\u0628\u062f\u0648\u0646 \u062f\u0627\u062f\u0647",
|
"no_data": "بدون داده",
|
||||||
"uncategorized_client": "\u0628\u062f\u0648\u0646 \u0645\u0634\u062a\u0631\u06cc",
|
"uncategorized_client": "بدون مشتری",
|
||||||
"uncategorized_project": "\u0628\u062f\u0648\u0646 \u067e\u0631\u0648\u0698\u0647",
|
"uncategorized_project": "بدون پروژه",
|
||||||
"uncategorized_tag": "\u0628\u062f\u0648\u0646 \u062a\u06af",
|
"uncategorized_tag": "بدون تگ",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,23 +129,23 @@ PERIOD_LABELS = {
|
|||||||
"period": "Custom period",
|
"period": "Custom period",
|
||||||
},
|
},
|
||||||
"fa": {
|
"fa": {
|
||||||
"this_week": "\u0627\u06cc\u0646 \u0647\u0641\u062a\u0647",
|
"this_week": "این هفته",
|
||||||
"this_month": "\u0627\u06cc\u0646 \u0645\u0627\u0647",
|
"this_month": "این ماه",
|
||||||
"this_year": "\u0627\u0645\u0633\u0627\u0644",
|
"this_year": "امسال",
|
||||||
"half_year_first": "\u0646\u06cc\u0645\u0647 \u0627\u0648\u0644 \u0633\u0627\u0644",
|
"half_year_first": "نیمه اول سال",
|
||||||
"half_year_second": "\u0646\u06cc\u0645\u0647 \u062f\u0648\u0645 \u0633\u0627\u0644",
|
"half_year_second": "نیمه دوم سال",
|
||||||
"period": "\u0628\u0627\u0632\u0647 \u062f\u0644\u062e\u0648\u0627\u0647",
|
"period": "بازه دلخواه",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
CURRENCY_LABELS = {
|
CURRENCY_LABELS = {
|
||||||
"USD": {"en": "USD", "fa": "\u062f\u0644\u0627\u0631 \u0622\u0645\u0631\u06cc\u06a9\u0627"},
|
"USD": {"en": "USD", "fa": "دلار آمریکا"},
|
||||||
"EUR": {"en": "EUR", "fa": "\u06cc\u0648\u0631\u0648"},
|
"EUR": {"en": "EUR", "fa": "یورو"},
|
||||||
"GBP": {"en": "GBP", "fa": "\u067e\u0648\u0646\u062f"},
|
"GBP": {"en": "GBP", "fa": "پوند"},
|
||||||
"IRR": {"en": "IRR", "fa": "\u0631\u06cc\u0627\u0644"},
|
"IRR": {"en": "IRR", "fa": "ریال"},
|
||||||
"IRT": {"en": "IRT", "fa": "\u062a\u0648\u0645\u0627\u0646"},
|
"IRT": {"en": "IRT", "fa": "تومان"},
|
||||||
"AED": {"en": "AED", "fa": "\u062f\u0631\u0647\u0645"},
|
"AED": {"en": "AED", "fa": "درهم"},
|
||||||
"TRY": {"en": "TRY", "fa": "\u0644\u06cc\u0631"},
|
"TRY": {"en": "TRY", "fa": "لیر"},
|
||||||
}
|
}
|
||||||
|
|
||||||
DECIMAL_TRIM_CURRENCIES = {"IRR", "IRT"}
|
DECIMAL_TRIM_CURRENCIES = {"IRR", "IRT"}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ def _money_label(locale: ExportLocale, income_totals: list[dict]) -> str:
|
|||||||
|
|
||||||
def _money_label_excel(locale: ExportLocale, income_totals: list[dict]) -> str:
|
def _money_label_excel(locale: ExportLocale, income_totals: list[dict]) -> str:
|
||||||
value = locale.format_money_label(income_totals, ascii_digits=True)
|
value = locale.format_money_label(income_totals, ascii_digits=True)
|
||||||
return f"\u202B{value}\u202C" if locale.is_rtl else value
|
return f"\u202B{value}\u202C" if locale.is_rtl else value # Unicode bidi control characters
|
||||||
|
|
||||||
|
|
||||||
def _rates_label(locale: ExportLocale, rates: list[dict], *, ascii_digits: bool = False) -> str:
|
def _rates_label(locale: ExportLocale, rates: list[dict], *, ascii_digits: bool = False) -> str:
|
||||||
@@ -114,7 +114,7 @@ def _rate_label_excel(locale: ExportLocale, rate: dict | None) -> str:
|
|||||||
f"{locale.format_amount_for_currency(rate['amount'], rate['currency'], ascii_digits=True)} "
|
f"{locale.format_amount_for_currency(rate['amount'], rate['currency'], ascii_digits=True)} "
|
||||||
f"{locale.currency_label(rate['currency'])}"
|
f"{locale.currency_label(rate['currency'])}"
|
||||||
)
|
)
|
||||||
return f"\u202B{value}\u202C" if locale.is_rtl else value
|
return f"\u202B{value}\u202C" if locale.is_rtl else value # Unicode bidi control characters
|
||||||
|
|
||||||
|
|
||||||
def _pdf_summary_rate_label(locale: ExportLocale, rates: list[dict]) -> str:
|
def _pdf_summary_rate_label(locale: ExportLocale, rates: list[dict]) -> str:
|
||||||
@@ -227,7 +227,7 @@ def _percentage_display(locale: ExportLocale, rows: list[dict], row_data: dict,
|
|||||||
|
|
||||||
def _percentage_value(locale: ExportLocale, percentage: str, *, ascii_digits: bool = False) -> str:
|
def _percentage_value(locale: ExportLocale, percentage: str, *, ascii_digits: bool = False) -> str:
|
||||||
value = f"{locale.format_amount(percentage, ascii_digits=ascii_digits)}%"
|
value = f"{locale.format_amount(percentage, ascii_digits=ascii_digits)}%"
|
||||||
return f"\u202B{value}\u202C" if locale.is_rtl and ascii_digits else value
|
return f"\u202B{value}\u202C" if locale.is_rtl and ascii_digits else value # Unicode bidi control characters
|
||||||
|
|
||||||
|
|
||||||
def _summary_breakdown_rows(
|
def _summary_breakdown_rows(
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ from apps.users.email_identity import normalize_email_identity
|
|||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
INVALID_MOBILE_FORMAT_MESSAGE = "\u0641\u0631\u0645\u062a \u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06cc\u0644 \u0646\u0627\u062f\u0631\u0633\u062a \u0627\u0633\u062a."
|
INVALID_MOBILE_FORMAT_MESSAGE = "فرمت شماره موبایل نادرست است."
|
||||||
INVALID_MOBILE_NUMBER_MESSAGE = "\u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06cc\u0644 \u0645\u0639\u062a\u0628\u0631 \u0646\u06cc\u0633\u062a."
|
INVALID_MOBILE_NUMBER_MESSAGE = "شماره موبایل معتبر نیست."
|
||||||
PASSWORD_MISMATCH_MESSAGE = "\u0631\u0645\u0632 \u0639\u0628\u0648\u0631 \u0645\u0637\u0627\u0628\u0642\u062a \u0646\u062f\u0627\u0631\u062f."
|
PASSWORD_MISMATCH_MESSAGE = "رمز عبور مطابقت ندارد."
|
||||||
NEW_PASSWORD_MISMATCH_MESSAGE = "\u0631\u0645\u0632 \u0639\u0628\u0648\u0631 \u062c\u062f\u06cc\u062f \u0648 \u062a\u06a9\u0631\u0627\u0631 \u0622\u0646 \u0645\u0637\u0627\u0628\u0642\u062a \u0646\u062f\u0627\u0631\u0646\u062f."
|
NEW_PASSWORD_MISMATCH_MESSAGE = "رمز عبور جدید و تکرار آن مطابقت ندارند."
|
||||||
PASSWORD_REUSE_MESSAGE = "\u0631\u0645\u0632 \u0639\u0628\u0648\u0631 \u062c\u062f\u06cc\u062f \u0646\u0628\u0627\u06cc\u062f \u0628\u0627 \u0631\u0645\u0632 \u0639\u0628\u0648\u0631 \u0642\u0628\u0644\u06cc \u06cc\u06a9\u0633\u0627\u0646 \u0628\u0627\u0634\u062f."
|
PASSWORD_REUSE_MESSAGE = "رمز عبور جدید نباید با رمز عبور قبلی یکسان باشد."
|
||||||
|
|
||||||
|
|
||||||
def _raise_password_validation_error(password: str, *, user, field_name: str) -> None:
|
def _raise_password_validation_error(password: str, *, user, field_name: str) -> None:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from apps.users.utils import record_login_attempt
|
|||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
USER_ALREADY_EXISTS_MESSAGE = "User already exists."
|
USER_ALREADY_EXISTS_MESSAGE = "User already exists."
|
||||||
PASSWORD_REUSE_MESSAGE = "\u0631\u0645\u0632 \u0639\u0628\u0648\u0631 \u062c\u062f\u06cc\u062f \u0646\u0628\u0627\u06cc\u062f \u0628\u0627 \u0631\u0645\u0632 \u0639\u0628\u0648\u0631 \u0642\u0628\u0644\u06cc \u06cc\u06a9\u0633\u0627\u0646 \u0628\u0627\u0634\u062f."
|
PASSWORD_REUSE_MESSAGE = "رمز عبور جدید نباید با رمز عبور قبلی یکسان باشد."
|
||||||
OTP_EXPIRY_SECONDS = 120
|
OTP_EXPIRY_SECONDS = 120
|
||||||
|
|
||||||
|
|
||||||
@@ -55,15 +55,15 @@ def register_user_with_password(mobile, password):
|
|||||||
def register_user_with_otp(mobile, code, password, first_name="", last_name=""):
|
def register_user_with_otp(mobile, code, password, first_name="", last_name=""):
|
||||||
"""Business logic for verifying OTP and registering a user."""
|
"""Business logic for verifying OTP and registering a user."""
|
||||||
if User.objects.filter(mobile=mobile).exists():
|
if User.objects.filter(mobile=mobile).exists():
|
||||||
raise ValidationError({"mobile": "\u0627\u06cc\u0646 \u0634\u0645\u0627\u0631\u0647 \u0642\u0628\u0644\u0627\u064b \u062b\u0628\u062a \u0634\u062f\u0647 \u0627\u0633\u062a."})
|
raise ValidationError({"mobile": "این شماره قبلاً ثبت شده است."})
|
||||||
|
|
||||||
redis_conn = get_redis_connection("default")
|
redis_conn = get_redis_connection("default")
|
||||||
stored_code = redis_conn.get(f"verification_code:{mobile}")
|
stored_code = redis_conn.get(f"verification_code:{mobile}")
|
||||||
|
|
||||||
if not stored_code:
|
if not stored_code:
|
||||||
raise ValidationError({"code": "\u06a9\u062f \u062a\u0623\u06cc\u06cc\u062f \u06cc\u0627\u0641\u062a \u0646\u0634\u062f."})
|
raise ValidationError({"code": "کد تأیید یافت نشد."})
|
||||||
if stored_code.decode("utf-8") != code:
|
if stored_code.decode("utf-8") != code:
|
||||||
raise ValidationError({"code": "\u06a9\u062f \u062a\u0623\u06cc\u06cc\u062f \u0627\u0634\u062a\u0628\u0627\u0647 \u0627\u0633\u062a."})
|
raise ValidationError({"code": "کد تأیید اشتباه است."})
|
||||||
|
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user(
|
||||||
mobile=mobile,
|
mobile=mobile,
|
||||||
@@ -84,10 +84,10 @@ def generate_and_send_otp(mobile, mode):
|
|||||||
user_exists = User.objects.filter(mobile=mobile).exists()
|
user_exists = User.objects.filter(mobile=mobile).exists()
|
||||||
|
|
||||||
if mode == "register" and user_exists:
|
if mode == "register" and user_exists:
|
||||||
raise ValidationError({"mobile": "\u0627\u06cc\u0646 \u0634\u0645\u0627\u0631\u0647 \u0642\u0628\u0644\u0627\u064b \u062b\u0628\u062a\u200c\u0646\u0627\u0645 \u0634\u062f\u0647 \u0627\u0633\u062a."})
|
raise ValidationError({"mobile": "این شماره قبلاً ثبتنام شده است."})
|
||||||
|
|
||||||
if mode in ["login", "forget_password"] and not user_exists:
|
if mode in ["login", "forget_password"] and not user_exists:
|
||||||
raise ValidationError({"mobile": "\u0627\u06cc\u0646 \u0634\u0645\u0627\u0631\u0647 \u06cc\u0627\u0641\u062a \u0646\u0634\u062f."})
|
raise ValidationError({"mobile": "این شماره یافت نشد."})
|
||||||
|
|
||||||
verification_code = "".join(random.choices(string.digits, k=5))
|
verification_code = "".join(random.choices(string.digits, k=5))
|
||||||
|
|
||||||
@@ -109,10 +109,10 @@ def login_with_password(mobile, password, request=None):
|
|||||||
|
|
||||||
if not user or not user.check_password(password):
|
if not user or not user.check_password(password):
|
||||||
record_login_attempt(request, user, LoginAttempt.StatusType.FAILED)
|
record_login_attempt(request, user, LoginAttempt.StatusType.FAILED)
|
||||||
raise ValidationError({"detail": "\u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06cc\u0644 \u06cc\u0627 \u0631\u0645\u0632 \u0639\u0628\u0648\u0631 \u0627\u0634\u062a\u0628\u0627\u0647 \u0627\u0633\u062a."})
|
raise ValidationError({"detail": "شماره موبایل یا رمز عبور اشتباه است."})
|
||||||
|
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
raise ValidationError({"detail": "\u062d\u0633\u0627\u0628 \u06a9\u0627\u0631\u0628\u0631\u06cc \u0634\u0645\u0627 \u063a\u06cc\u0631\u0641\u0639\u0627\u0644 \u0634\u062f\u0647 \u0627\u0633\u062a."})
|
raise ValidationError({"detail": "حساب کاربری شما غیرفعال شده است."})
|
||||||
|
|
||||||
record_login_attempt(request, user, LoginAttempt.StatusType.SUCCESS)
|
record_login_attempt(request, user, LoginAttempt.StatusType.SUCCESS)
|
||||||
return get_tokens_for_user(user)
|
return get_tokens_for_user(user)
|
||||||
@@ -125,7 +125,7 @@ def login_with_otp(mobile, code, request=None):
|
|||||||
|
|
||||||
if not stored_code or stored_code.decode("utf-8") != code:
|
if not stored_code or stored_code.decode("utf-8") != code:
|
||||||
record_login_attempt(request, None, LoginAttempt.StatusType.FAILED)
|
record_login_attempt(request, None, LoginAttempt.StatusType.FAILED)
|
||||||
raise ValidationError({"code": "\u06a9\u062f \u062a\u0627\u06cc\u06cc\u062f \u0646\u0627\u0645\u0639\u062a\u0628\u0631 \u0627\u0633\u062a \u06cc\u0627 \u0645\u0646\u0642\u0636\u06cc \u0634\u062f\u0647 \u0627\u0633\u062a."})
|
raise ValidationError({"code": "کد تایید نامعتبر است یا منقضی شده است."})
|
||||||
|
|
||||||
user, created = User.objects.get_or_create(mobile=mobile)
|
user, created = User.objects.get_or_create(mobile=mobile)
|
||||||
if created:
|
if created:
|
||||||
@@ -133,7 +133,7 @@ def login_with_otp(mobile, code, request=None):
|
|||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
raise ValidationError({"detail": "\u062d\u0633\u0627\u0628 \u06a9\u0627\u0631\u0628\u0631\u06cc \u0634\u0645\u0627 \u063a\u06cc\u0631\u0641\u0639\u0627\u0644 \u0634\u062f\u0647 \u0627\u0633\u062a."})
|
raise ValidationError({"detail": "حساب کاربری شما غیرفعال شده است."})
|
||||||
|
|
||||||
record_login_attempt(request, user, LoginAttempt.StatusType.SUCCESS)
|
record_login_attempt(request, user, LoginAttempt.StatusType.SUCCESS)
|
||||||
redis_conn.delete(f"verification_code:{mobile}")
|
redis_conn.delete(f"verification_code:{mobile}")
|
||||||
@@ -145,13 +145,13 @@ def reset_password_with_otp(mobile, code, password):
|
|||||||
"""Verify OTP and change forgotten password."""
|
"""Verify OTP and change forgotten password."""
|
||||||
user = User.objects.filter(mobile=mobile).first()
|
user = User.objects.filter(mobile=mobile).first()
|
||||||
if not user:
|
if not user:
|
||||||
raise ValidationError({"mobile": "\u06a9\u0627\u0631\u0628\u0631\u06cc \u0628\u0627 \u0627\u06cc\u0646 \u0634\u0645\u0627\u0631\u0647 \u06cc\u0627\u0641\u062a \u0646\u0634\u062f."})
|
raise ValidationError({"mobile": "کاربری با این شماره یافت نشد."})
|
||||||
|
|
||||||
redis_conn = get_redis_connection("default")
|
redis_conn = get_redis_connection("default")
|
||||||
stored_code = redis_conn.get(f"verification_code:{mobile}")
|
stored_code = redis_conn.get(f"verification_code:{mobile}")
|
||||||
|
|
||||||
if not stored_code or stored_code.decode("utf-8") != code:
|
if not stored_code or stored_code.decode("utf-8") != code:
|
||||||
raise ValidationError({"code": "\u06a9\u062f \u062a\u0627\u06cc\u06cc\u062f \u0646\u0627\u0645\u0639\u062a\u0628\u0631 \u0627\u0633\u062a \u06cc\u0627 \u0645\u0646\u0642\u0636\u06cc \u0634\u062f\u0647 \u0627\u0633\u062a."})
|
raise ValidationError({"code": "کد تایید نامعتبر است یا منقضی شده است."})
|
||||||
|
|
||||||
_validate_new_password(password, user=user, field_name="password")
|
_validate_new_password(password, user=user, field_name="password")
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ def reset_password_with_otp(mobile, code, password):
|
|||||||
def change_password(user, old_password, new_password):
|
def change_password(user, old_password, new_password):
|
||||||
"""Change password for an already authenticated user."""
|
"""Change password for an already authenticated user."""
|
||||||
if not user.check_password(old_password):
|
if not user.check_password(old_password):
|
||||||
raise ValidationError({"old_password": "\u0631\u0645\u0632 \u0639\u0628\u0648\u0631 \u0641\u0639\u0644\u06cc \u0627\u0634\u062a\u0628\u0627\u0647 \u0627\u0633\u062a."})
|
raise ValidationError({"old_password": "رمز عبور فعلی اشتباه است."})
|
||||||
|
|
||||||
_validate_new_password(new_password, user=user, field_name="new_password")
|
_validate_new_password(new_password, user=user, field_name="new_password")
|
||||||
|
|
||||||
@@ -182,9 +182,9 @@ def change_password(user, old_password, new_password):
|
|||||||
def logout_user(refresh_token_str):
|
def logout_user(refresh_token_str):
|
||||||
"""Blacklist the user's refresh token."""
|
"""Blacklist the user's refresh token."""
|
||||||
if not refresh_token_str:
|
if not refresh_token_str:
|
||||||
raise ValidationError({"refresh": "\u062a\u0648\u06a9\u0646 \u0631\u0641\u0631\u0634 \u0627\u0644\u0632\u0627\u0645\u06cc \u0627\u0633\u062a."})
|
raise ValidationError({"refresh": "توکن رفرش الزامی است."})
|
||||||
try:
|
try:
|
||||||
token = RefreshToken(refresh_token_str)
|
token = RefreshToken(refresh_token_str)
|
||||||
token.blacklist()
|
token.blacklist()
|
||||||
except TokenError:
|
except TokenError:
|
||||||
raise ValidationError({"detail": "\u062a\u0648\u06a9\u0646 \u0646\u0627\u0645\u0639\u062a\u0628\u0631 \u0627\u0633\u062a \u06cc\u0627 \u0642\u0628\u0644\u0627 \u0645\u0646\u0642\u0636\u06cc \u0634\u062f\u0647 \u0627\u0633\u062a."})
|
raise ValidationError({"detail": "توکن نامعتبر است یا قبلا منقضی شده است."})
|
||||||
|
|||||||
Reference in New Issue
Block a user