From 5bfc52563d9ad17891930a3395205c18735c6334 Mon Sep 17 00:00:00 2001 From: fatmaebrahim Date: Thu, 4 Jun 2026 15:11:59 +0300 Subject: [PATCH 1/6] feat: added migration for hidden_from and added admin api endpoints to set hidden_from Co-authored-by: Copilot --- .../cshr/migrations/0037_user_hidden_from.py | 23 ++++ server/cshr/models/users.py | 1 + server/cshr/routes/users.py | 2 + server/cshr/services/event.py | 14 +-- server/cshr/services/landing_page.py | 26 +++-- server/cshr/services/meetings.py | 30 +++-- server/cshr/services/users.py | 105 ++++++------------ server/cshr/services/vacations.py | 70 +++--------- server/cshr/views/meetings.py | 6 +- server/cshr/views/users.py | 65 +++++++++-- server/cshr/views/vacations.py | 2 +- 11 files changed, 173 insertions(+), 171 deletions(-) create mode 100644 server/cshr/migrations/0037_user_hidden_from.py diff --git a/server/cshr/migrations/0037_user_hidden_from.py b/server/cshr/migrations/0037_user_hidden_from.py new file mode 100644 index 000000000..533313409 --- /dev/null +++ b/server/cshr/migrations/0037_user_hidden_from.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.17 on 2026-06-03 16:34 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cshr", "0036_update_excuse_balance"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="hidden_from", + field=models.ManyToManyField( + blank=True, + related_name="hidden_from_users", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/server/cshr/models/users.py b/server/cshr/models/users.py index d56989e8e..c4a5871e1 100644 --- a/server/cshr/models/users.py +++ b/server/cshr/models/users.py @@ -114,6 +114,7 @@ class User(AbstractBaseUser, TimeStamp): is_superuser = models.BooleanField(default=False) is_active = models.BooleanField(default=True) objects = CshrBaseUserManger() + hidden_from = models.ManyToManyField("User",related_name="hidden_from_users", blank=True) @property def full_name(self) -> str: diff --git a/server/cshr/routes/users.py b/server/cshr/routes/users.py index 59ddc395e..f26b1e5c4 100644 --- a/server/cshr/routes/users.py +++ b/server/cshr/routes/users.py @@ -18,6 +18,7 @@ PostUserSkillsAPIView, GetUsersBirthDatesAPIView, AllActiveUsersAPIView, + HiddenFromUserAPIView, ) @@ -36,6 +37,7 @@ path("team/supervisors/", TeamSupervisorsAPIView.as_view()), path("supervisors/", SupervisorsAPIView.as_view()), path("supervisor//", SupervisorUserAPIView.as_view()), + path("admin//hidden_from/", HiddenFromUserAPIView.as_view()), path("admin//", AdminUserAPIView.as_view()), path("/", GeneralUserAPIView.as_view()), ] diff --git a/server/cshr/services/event.py b/server/cshr/services/event.py index 900d461d9..d566783ce 100644 --- a/server/cshr/services/event.py +++ b/server/cshr/services/event.py @@ -1,13 +1,12 @@ """This file contains everything related to the Event model.""" +from django.db.models.query import QuerySet import datetime from cshr.models.event import Event -from typing import List -from cshr.models.users import User -def get_event_by_id(id: str) -> Event: +def get_event_by_id(id: str) -> Event | None: """Return event who have the same id""" try: return Event.objects.get(id=int(id)) @@ -15,22 +14,21 @@ def get_event_by_id(id: str) -> Event: return None -def get_all_events() -> Event: +def get_all_events() -> QuerySet[Event]: """Return all events""" return Event.objects.all() -def filter_events_by_month_and_year(user: User, month: str, year: str) -> Event: +def filter_events_by_month_and_year(month: str, year: str) -> QuerySet[Event]: """ This function will filter all of events based on its yesr, month. """ - events: List[Event] = Event.objects.filter( + return Event.objects.filter( from_date__month=month, from_date__year=year ) - return events -def filter_events_by_day(day: int) -> List[Event]: +def filter_events_by_day(day: int) -> QuerySet[Event]: """Filter all users by birthdates""" today = datetime.datetime.now() return Event.objects.filter( diff --git a/server/cshr/services/landing_page.py b/server/cshr/services/landing_page.py index ca63124aa..3999eca8f 100644 --- a/server/cshr/services/landing_page.py +++ b/server/cshr/services/landing_page.py @@ -24,31 +24,35 @@ def landing_page_calendar_functionality(user: User, month: str, year: str) -> Li response: List[Any] = [] # Fetch vacations - vacations = filter_vacations_by_month_and_year(month, year).order_by("-created_at") + vacations = filter_vacations_by_month_and_year( + month, year, requesting_user=user + ).order_by("-created_at") for vacation in vacations: vacation_data = wrap_vacation_request(vacation) response.append(vacation_data) # Fetch meetings - meetings = filter_meetings_by_month_and_year(user, month, year).order_by( - "-created_at" - ) + meetings = filter_meetings_by_month_and_year( + month, year, requesting_user=user + ).order_by("-created_at") for meeting in meetings: meeting_data = wrap_meeting_request(meeting) response.append(meeting_data) + # Fetch users' birthdays + birthdates = filter_users_by_birth_month(month, requesting_user=user).order_by( + "-created_at" + ) # type: ignore + for birthday_user in birthdates: + birthday_data = wrap_birthday_event(birthday_user) + response.append(birthday_data) + # Fetch events - events = filter_events_by_month_and_year(user, month, year).order_by("-created_at") + events = filter_events_by_month_and_year(month, year).order_by("-created_at") # type: ignore for event in events: event_data = wrap_event_request(event) response.append(event_data) - # Fetch users' birthdays - users_birthdates = filter_users_by_birth_month(month).order_by("-created_at") - for birthday_user in users_birthdates: - birthday_data = wrap_birthday_event(birthday_user) - response.append(birthday_data) - # Fetch public holidays public_holidays = filter_public_holidays_by_month_and_year(year, month).order_by( "-created_at" diff --git a/server/cshr/services/meetings.py b/server/cshr/services/meetings.py index 4ff8c1b4e..f67b19db9 100644 --- a/server/cshr/services/meetings.py +++ b/server/cshr/services/meetings.py @@ -1,12 +1,12 @@ """This file contains everything related to the Meetings model.""" +from typing import Optional +from django.db.models.query import QuerySet from cshr.models.meetings import Meetings -from typing import List - from cshr.models.users import User -def get_meeting_by_id(id: str) -> Meetings: +def get_meeting_by_id(id: str) -> Meetings | None: """Return meeting who have the same id""" try: return Meetings.objects.get(id=int(id)) @@ -14,21 +14,29 @@ def get_meeting_by_id(id: str) -> Meetings: return None -def get_all_meetings() -> Meetings: +def get_all_meetings(requesting_user: Optional[User] = None) -> QuerySet[Meetings]: """Return all meetings""" - return Meetings.objects.all() + queryset = Meetings.objects.all() + if requesting_user: + queryset = queryset.exclude(host_user__hidden_from=requesting_user) + return queryset -def filter_meetings_by_month_and_year(user: User, month: str, year: str) -> Meetings: +def filter_meetings_by_month_and_year(month: str, year: str, requesting_user: Optional[User] = None) -> QuerySet[Meetings]: """ This function will filter all of meetings based on its yesr, month. """ - meetings: List[Meetings] = Meetings.objects.filter( + queryset = Meetings.objects.filter( date__month=month, date__year=year ) - return meetings + if requesting_user: + queryset = queryset.exclude(host_user__hidden_from=requesting_user) + return queryset -def filter_meetings_by_day(year: int, month: int, day: int) -> List[Meetings]: - """Filter all users by birthdates""" - return Meetings.objects.filter(date__year=year, date__month=month, date__day=day) +def filter_meetings_by_day(year: int, month: int, day: int, requesting_user: Optional[User] = None) -> QuerySet[Meetings]: + """Filter meetings by day""" + queryset = Meetings.objects.filter(date__year=year, date__month=month, date__day=day) + if requesting_user: + queryset = queryset.exclude(host_user__hidden_from=requesting_user) + return queryset diff --git a/server/cshr/services/users.py b/server/cshr/services/users.py index f94272e74..d2961e4d6 100644 --- a/server/cshr/services/users.py +++ b/server/cshr/services/users.py @@ -1,16 +1,15 @@ """This file will containes everything related to User model.""" -from typing import Dict, List, Union, Optional +from typing import Dict, List, Optional -from django.contrib.auth.hashers import check_password -from django.db.models import Q, F +from django.db.models import Q from django.db.models.query import QuerySet from cshr.models.office import Office from cshr.models.users import USER_TYPE, User, UserSkills -def get_user_by_id(id: str) -> User: +def get_user_by_id(id: str) -> User | None: """Return user who have the same id""" if id is None: return id @@ -20,7 +19,7 @@ def get_user_by_id(id: str) -> User: return None -def filter_users_by_ids(ids: List[str]) -> QuerySet[User]: +def filter_users_by_ids(ids: List[str]) -> QuerySet[User] | None: """Return users who have the same id""" try: return User.objects.filter(id__in=ids) @@ -28,34 +27,14 @@ def filter_users_by_ids(ids: List[str]) -> QuerySet[User]: return None -def get_user_by_email(email: str) -> User: +def get_user_by_email(email: str) -> User | None: """Return user who have the same email""" try: return User.objects.get(email=email) except User.DoesNotExist: return None - -def get_user_by_full_name(first_name: str, last_name: str) -> User: - """Return user who have the same email""" - try: - return User.objects.get(first_name=first_name, last_name=last_name) - except User.DoesNotExist: - return None - - -def success_login_user(email, password) -> User: - """Return user who have the same email and password""" - - user = get_user_by_email(email=email) - if user is not None: - if check_password(password, user.password): - return user - return None - return None - - -def get_user_type_by_id(id: str) -> User: +def get_user_type_by_id(id: str) -> str | None: """Return user type by id""" try: user = User.objects.get(id=int(id)) @@ -63,29 +42,19 @@ def get_user_type_by_id(id: str) -> User: except User.DoesNotExist: return None - -def filter_users_by_birth_month(month: str) -> User: +def filter_users_by_birth_month(month: str, requesting_user: Optional[User] = None) -> QuerySet[User]: """Filter users based on birthdayes.""" - users: List[User] = User.objects.filter(birthday__month=month, user_type__in=[USER_TYPE.USER, USER_TYPE.SUPERVISOR]) - return users - - -def get_users_filter( - search_input: str, -) -> User: - """Return users by filters""" - - users = User.objects.filter( - Q(email__icontains=search_input) - | Q(first_name__icontains=search_input) - | Q(last_name__icontains=search_input) - ) + users = User.objects.filter(birthday__month=month, user_type__in=[USER_TYPE.USER, USER_TYPE.SUPERVISOR]) + if requesting_user: + users = users.exclude(hidden_from=requesting_user) return users -def get_all_of_users(options: Optional[Dict] = None): +def get_all_of_users(options: Optional[Dict] = None, requesting_user: Optional[User] = None) -> QuerySet[User]: """Return all users based on filtering options.""" queryset = User.objects.filter(is_active=True) + if requesting_user: + queryset = queryset.exclude(hidden_from=requesting_user) # Handle optional filtering if options: @@ -114,22 +83,22 @@ def get_all_of_users(options: Optional[Dict] = None): return queryset -def get_admin_office_users(admin: User) -> User: +def get_admin_office_users(admin: User) -> QuerySet[User]: """Return all users who working in the same office of the admin""" - location = Office.objects.get(id=admin.location.id) + location = Office.objects.get(id=admin.location.id) # type: ignore return ( User.objects.filter(location=location) - .exclude(id__in=[admin.id]) + .exclude(id__in=[admin.id]) # type: ignore .order_by("first_name") ) -def get_active_admin_office_users(admin: User) -> User: +def get_active_admin_office_users(admin: User) -> QuerySet[User]: """Return all active users who working in the same office of the admin""" - location = Office.objects.get(id=admin.location.id) + location = Office.objects.get(id=admin.location.id) # type: ignore return ( User.objects.filter(location=location, is_active=True) - .exclude(id__in=[admin.id]) + .exclude(id__in=[admin.id]) # type: ignore .order_by("first_name") ) @@ -139,16 +108,16 @@ def get_or_create_skill_by_name(name: str) -> UserSkills or bool: # type: ignor return UserSkills.objects.get_or_create(name=name.lower()) -def get_user_team_members(user: User) -> List[User]: +def get_user_team_members(user: User) -> QuerySet[User]: """Return a list of members and team leaders""" - # , user_type=USER_TYPE.USER - members: List[User] = User.objects.filter(team=user.team).order_by("-created_at") + members = User.objects.filter(team=user.team).exclude(hidden_from=user).order_by("-created_at") return members -def get_user_team_leads(user: User) -> Union[List[User], List]: - team_leaders: List[User] = ( - get_user_by_id(user.id).reporting_to.all().order_by("-created_at") +def get_user_team_leads(user: User) -> QuerySet[User]: + """Return a list of team leaders of a user""" + team_leaders = ( + get_user_by_id(user.id).reporting_to.all().order_by("-created_at") # type: ignore ) return team_leaders @@ -158,14 +127,12 @@ def get_all_skills(): return UserSkills.objects.all() -def filter_users_by_birthdates(month: int, day: int) -> List[User]: +def filter_users_by_birthdates(month: int, day: int, requesting_user: Optional[User] = None) -> QuerySet[User]: """Filter all users by birthdates""" - return User.objects.filter(birthday__month=month, birthday__day=day) - - -def get_supervisors() -> QuerySet[User]: - """Return all supervisors users""" - return User.objects.filter(user_type=USER_TYPE.SUPERVISOR) + queryset = User.objects.filter(birthday__month=month, birthday__day=day) + if requesting_user: + queryset = queryset.exclude(hidden_from=requesting_user) + return queryset def get_admins_and_supervisors() -> QuerySet[User]: @@ -190,10 +157,10 @@ def build_user_reporting_to_hierarchy(user: User, visited=None) -> List[int]: visited = set() # Avoid infinite loops by checking if the user has already been visited - if user.id in visited: + if user.id in visited: # type: ignore return [] - visited.add(user.id) + visited.add(user.id) # type: ignore hierarchy = [] for report_user in user.reporting_to.all(): @@ -206,7 +173,7 @@ def build_user_reporting_to_hierarchy(user: User, visited=None) -> List[int]: def filter_admins_same_office_of_the_user(user: User) -> QuerySet[User]: """Return all admins of the same office of the user.""" - return User.objects.filter(location__id=user.location.id, user_type=USER_TYPE.ADMIN) + return User.objects.filter(location__id=user.location.id, user_type=USER_TYPE.ADMIN) # type: ignore def build_user_reporting_to_hierarchy_down(user: User, visited=None) -> List[int]: @@ -225,10 +192,10 @@ def build_user_reporting_to_hierarchy_down(user: User, visited=None) -> List[int visited = set() # Avoid infinite loops by checking if the user has already been visited - if user.id in visited: + if user.id in visited: # type: ignore return [] - visited.add(user.id) + visited.add(user.id) # type: ignore reports = [] @@ -236,7 +203,7 @@ def build_user_reporting_to_hierarchy_down(user: User, visited=None) -> List[int direct_reports = User.objects.filter(reporting_to=user) for report in direct_reports: - reports.append(report.id) # Add the direct report's ID + reports.append(report.id) # type: ignore # Add the direct report's ID reports.extend( build_user_reporting_to_hierarchy_down(report, visited) ) # Recursively add all indirect reports diff --git a/server/cshr/services/vacations.py b/server/cshr/services/vacations.py index bda215e12..ae800cb56 100644 --- a/server/cshr/services/vacations.py +++ b/server/cshr/services/vacations.py @@ -1,15 +1,13 @@ """This file contains everything related to the Vacation model.""" -from cshr.models.requests import STATUS_CHOICES, Requests -from cshr.models.users import User +from cshr.models.requests import STATUS_CHOICES from django.db.models import Q from django.db.models.query import QuerySet -from typing import List -from cshr.models.vacations import ReasonChoices, UserVacationBalance, Vacation +from cshr.models.vacations import ReasonChoices, Vacation -def filter_vacations_by_month_and_year(month: str, year: str) -> Vacation: +def filter_vacations_by_month_and_year(month: str, year: str, requesting_user=None) -> QuerySet[Vacation]: """ This function will filter all of vacations based on its yesr, month. """ @@ -23,16 +21,19 @@ def filter_vacations_by_month_and_year(month: str, year: str) -> Vacation: end_date__year=year, ) ) - return vacations.exclude( + queryset = vacations.exclude( status__in=[ STATUS_CHOICES.CANCELED, STATUS_CHOICES.REJECTED, STATUS_CHOICES.CANCEL_APPROVED, ] ) + if requesting_user: + queryset = queryset.exclude(applying_user__hidden_from=requesting_user) + return queryset -def get_vacation_by_id(id: str) -> Vacation: +def get_vacation_by_id(id: str) -> Vacation | None: """Return vacation who have the same id""" try: return Vacation.objects.get(id=int(id)) @@ -40,62 +41,19 @@ def get_vacation_by_id(id: str) -> Vacation: return None -def get_all_vacations() -> Vacation: +def get_all_vacations(requesting_user=None) -> QuerySet[Vacation]: """Return all vacations""" - return Vacation.objects.all() - - -def filter_vacations_by_pending_status(team_lead_user: User) -> Vacation: - """Return all vacations that has pending status""" - users_reporting_to_teemlead_ids: List[int] = User.objects.filter( - reporting_to=team_lead_user - ).values_list("id", flat=True) - return Vacation.objects.filter( - status=STATUS_CHOICES.PENDING, - applying_user__id__in=users_reporting_to_teemlead_ids, - ) + queryset = Vacation.objects.all() + if requesting_user: + queryset = queryset.exclude(applying_user__hidden_from=requesting_user) + return queryset def filter_vacations_by_user_id(id: str) -> QuerySet[Vacation]: "Return all vacations for certain user" return Vacation.objects.filter(applying_user=id).order_by("created_at") - -def get_vacation_based_on_request(request_: Requests): - """Returns vacation object who created at the sane time of request creation.""" - try: - return Vacation.objects.get( - # created_at__day = request_.created_at.day, - applying_user__id=request_.applying_user.id, - approval_user__id=request_.approval_user.id, - ) - except Vacation.DoesNotExist: - return None - - -def filter_user_vacations_by_pending_status(user: User) -> Vacation: - """Return all vacations that has pending status and related to user""" - return Vacation.objects.filter( - status=STATUS_CHOICES.PENDING, - applying_user=user, - ).order_by("created_at") - - -def filter_user_vacations(user: User) -> Vacation: - """Return all vacations that has pending status and related to user""" - return Vacation.objects.filter(applying_user=user).order_by("created_at") - - -def get_user_balance(user: User) -> UserVacationBalance: - """ - Return user balance - """ - try: - return UserVacationBalance.objects.get(user=user) - except UserVacationBalance.DoesNotExist: - return None - -def normalize_request_type(request_type: str) -> str: +def normalize_request_type(request_type: str) -> str | None: """ Normalize request type to be used in the query """ diff --git a/server/cshr/views/meetings.py b/server/cshr/views/meetings.py index 1f439e4db..159757d89 100644 --- a/server/cshr/views/meetings.py +++ b/server/cshr/views/meetings.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict from cshr.serializers.meetings import MeetingsSerializer from cshr.models.users import User from cshr.api.permission import UserIsAuthenticated @@ -45,7 +45,7 @@ def post(self, request: Request) -> Response: ) def get_queryset(self) -> Response: - query_set = get_all_meetings() + query_set = get_all_meetings(requesting_user=self.request.user) return query_set @@ -112,7 +112,7 @@ def get(self, request: Request) -> Response: year: int = int(request.query_params.get("year")) month: int = int(request.query_params.get("month")) day: int = int(request.query_params.get("day")) - events: List[User] = filter_meetings_by_day(year, month, day) + events = filter_meetings_by_day(year, month, day, requesting_user=request.user) serializer = self.serializer_class(events, many=True) return CustomResponse.success( data=serializer.data, message="Meetings founded successfully." diff --git a/server/cshr/views/users.py b/server/cshr/views/users.py index ebcf0a231..f9d870932 100644 --- a/server/cshr/views/users.py +++ b/server/cshr/views/users.py @@ -1,9 +1,8 @@ -from typing import List from django.db.models import Q from rest_framework.generics import GenericAPIView, ListAPIView from rest_framework.request import Request from rest_framework.response import Response -from cshr.models.users import USER_TYPE, User, UserSkills +from cshr.models.users import USER_TYPE, User from cshr.api.permission import ( IsAdmin, IsSupervisor, @@ -62,9 +61,9 @@ def get_queryset(self) -> Response: if len(location_id) > 0 or len(team_name) > 0: options = {"location": {"id": location_id}, "team": {"name": team_name}} - query_set = get_all_of_users(options) + query_set = get_all_of_users(options, requesting_user=self.request.user) else: - query_set = get_all_of_users() + query_set = get_all_of_users(requesting_user=self.request.user) # Filter by search query (full name) if user_full_name: @@ -92,7 +91,7 @@ def get_queryset(self) -> Response: Get all team information, Team leaders and team members """ user: User = self.request.user - query_set: List[User] = get_user_team_members(user) + query_set = get_user_team_members(user) return query_set @@ -105,7 +104,7 @@ def get_queryset(self) -> Response: Get all team information, Team leaders and team members """ user: User = self.request.user - query_set: List[User] = get_user_team_leads(user) + query_set = get_user_team_leads(user) return query_set @@ -340,7 +339,7 @@ def put(self, request: Request, id: str, format=None) -> Response: serializer = self.get_serializer(user, data=request.data, partial=True) if serializer.is_valid(): office = get_office_by_id(request.data.get("location")) - user: User = serializer.save( + user = serializer.save( team=request.data.get("team"), gender=request.data.get("gender"), location=office, @@ -361,7 +360,7 @@ class UserSkillsAPIView(GenericAPIView): serializer_class = UserSkillsSerializer def get(self, request: Request): - skills: UserSkills = get_all_skills() + skills = get_all_skills() serializer: UserSkillsSerializer = self.get_serializer(skills, many=True) return CustomResponse.success( data=serializer.data, message="Success found skills" @@ -376,8 +375,10 @@ def post(self, request: Request): """to add a skill to a user""" serializer = self.get_serializer(data=request.data) if serializer.is_valid(): - user_id: int = request.data.get("user_id") - user: User = get_user_by_id(user_id) + user_id = request.data.get("user_id") + user = get_user_by_id(str(user_id)) + if not user: + return CustomResponse.not_found(message="User not found") skills = serializer.validated_data.get("skills") if type(skills) is not list: skills = [skills] @@ -415,7 +416,7 @@ def get(self, request: Request) -> Response: ) month: int = int(request.query_params.get("month")) day: int = int(request.query_params.get("day")) - users: List[User] = filter_users_by_birthdates(month, day) + users = filter_users_by_birthdates(month, day, requesting_user=request.user) serializer = self.serializer_class(users, many=True) return CustomResponse.success( data=serializer.data, message="Users founded successfully." @@ -456,6 +457,46 @@ def get(self, request: Request) -> Response: # queryset = User.objects.filter(is_active=True) # ... and handles options. - query_set = get_all_of_users() + query_set = get_all_of_users(requesting_user=request.user) serializer = self.get_serializer(query_set, many=True) return CustomResponse.success(data=serializer.data) + +class HiddenFromUserAPIView(GenericAPIView): + permission_classes = [IsAdmin] + serializer_class = GeneralUserSerializer + + def put(self, request: Request, id: str) -> Response: + """Update user hidden from list""" + user = get_user_by_id(id) + if user is None: + return CustomResponse.not_found(status_code=404, message="User not found") + + new_hidden_from_ids = request.data.get("hidden_from") + current_ids = set(user.hidden_from.values_list("id", flat=True)) + if current_ids == set(new_hidden_from_ids): + return CustomResponse.success( + data=self.get_serializer(user.hidden_from.all(), many=True).data, + message="No changes", + status_code=200, + ) + + users = User.objects.filter(id__in=new_hidden_from_ids) + user.hidden_from.set(users) + return CustomResponse.success( + data=self.get_serializer(user.hidden_from.all(), many=True).data, + message="Hidden from list updated.", + status_code=200, + ) + + def get(self, request: Request, id: str) -> Response: + """Get user hidden from list""" + user = get_user_by_id(id) + if user is None: + return CustomResponse.not_found(status_code=404, message="User not found") + + hidden_from_users = user.hidden_from.all() + return CustomResponse.success( + data=self.get_serializer(hidden_from_users, many=True).data, + message="User hidden from list found", + status_code=200, + ) \ No newline at end of file diff --git a/server/cshr/views/vacations.py b/server/cshr/views/vacations.py index 1881721dd..a36ed35eb 100644 --- a/server/cshr/views/vacations.py +++ b/server/cshr/views/vacations.py @@ -121,7 +121,7 @@ def post(self, request: Request) -> Response: def get_queryset(self) -> Response: """method to get all vacations""" - query_set: List[Vacation] = get_all_vacations() + query_set = get_all_vacations(requesting_user=self.request.user) return query_set From c7e04f59ece32e1a6375468b6325b62aca5a9473 Mon Sep 17 00:00:00 2001 From: fatmaebrahim Date: Thu, 4 Jun 2026 17:58:28 +0300 Subject: [PATCH 2/6] test: added hidden from tests Co-authored-by: Copilot --- server/cshr/tests/test_hidden_from.py | 411 ++++++++++++++++++++++++++ server/cshr/views/users.py | 12 +- 2 files changed, 420 insertions(+), 3 deletions(-) create mode 100644 server/cshr/tests/test_hidden_from.py diff --git a/server/cshr/tests/test_hidden_from.py b/server/cshr/tests/test_hidden_from.py new file mode 100644 index 000000000..1a4051af6 --- /dev/null +++ b/server/cshr/tests/test_hidden_from.py @@ -0,0 +1,411 @@ +from rest_framework import status +from rest_framework.test import APITestCase +from cshr.models.users import User +from cshr.models.office import Office +from cshr.models.meetings import Meetings +from cshr.models.vacations import Vacation +from django.contrib.auth.hashers import make_password + + +class HiddenFromAPITests(APITestCase): + """Tests for the hidden_from feature.""" + + def setUp(self): + office = Office.objects.create( + name="Egypt", country="Egypt", weekend="Friday:Saturday" + ) + self.user_a1 = User.objects.create( + first_name="User", + last_name="A1", + email="usera1@ex.com", + password=make_password("12345678"), + mobile_number="123456789123", + telegram_link="@usera1", + team="Business Development", + user_type="User", + address="---", + social_insurance_number="---", + job_title="Developer", + gender="Male", + birthday="2000-03-10", + joining_at="2022-01-01", + location=office, + ) + self.user_a2 = User.objects.create( + first_name="User", + last_name="A2", + email="usera2@ex.com", + password=make_password("12345678"), + mobile_number="123456789123", + telegram_link="@usera2", + team="Business Development", + user_type="User", + address="---", + social_insurance_number="---", + job_title="Developer", + gender="Male", + birthday="1998-07-20", + joining_at="2022-03-01", + location=office, + ) + self.user_b1 = User.objects.create( + first_name="User", + last_name="B1", + email="userb1@ex.com", + password=make_password("12345678"), + mobile_number="123456789123", + telegram_link="@userb1", + team="HR & Finance", + user_type="User", + address="---", + social_insurance_number="---", + job_title="HR Specialist", + gender="Female", + birthday="1999-05-15", + joining_at="2021-06-01", + location=office, + ) + self.user_b2 = User.objects.create( + first_name="User", + last_name="B2", + email="userb2@ex.com", + password=make_password("12345678"), + mobile_number="123456789123", + telegram_link="@userb2", + team="HR & Finance", + user_type="User", + address="---", + social_insurance_number="---", + job_title="HR Specialist", + gender="Female", + birthday="2001-11-05", + joining_at="2023-01-01", + location=office, + ) + self.super_a = User.objects.create( + first_name="Supervisor", + last_name="A", + email="supera@ex.com", + password=make_password("12345678"), + mobile_number="123456789123", + telegram_link="@supera", + team="Business Development", + user_type="Supervisor", + address="---", + social_insurance_number="---", + job_title="Team Lead", + gender="Male", + birthday="1995-08-12", + joining_at="2020-01-01", + location=office, + ) + self.super_b = User.objects.create( + first_name="Supervisor", + last_name="B", + email="superb@ex.com", + password=make_password("12345678"), + mobile_number="123456789123", + telegram_link="@superb", + team="HR & Finance", + user_type="Supervisor", + address="---", + social_insurance_number="---", + job_title="Team Lead", + gender="Female", + birthday="1993-02-28", + joining_at="2019-06-01", + location=office, + ) + self.admin_a = User.objects.create( + first_name="AdminUser", + last_name="A", + email="admina@ex.com", + password=make_password("12345678"), + mobile_number="123456789123", + telegram_link="@admina", + team="Business Development", + user_type="Admin", + address="---", + social_insurance_number="---", + job_title="Manager", + gender="Male", + birthday="1990-01-01", + joining_at="2018-01-01", + location=office, + ) + self.admin_b = User.objects.create( + first_name="AdminUser", + last_name="B", + email="adminb@ex.com", + password=make_password("12345678"), + mobile_number="123456789123", + telegram_link="@adminb", + team="HR & Finance", + user_type="Admin", + address="---", + social_insurance_number="---", + job_title="Manager", + gender="Female", + birthday="1992-06-15", + joining_at="2018-06-01", + location=office, + ) + + self.user_a1.reporting_to.add(self.super_a) + self.user_a2.reporting_to.add(self.super_a) + self.user_b1.reporting_to.add(self.super_b) + self.user_b2.reporting_to.add(self.super_b) + + self.token_admin_a = self._get_token("admina@ex.com", "12345678") + self.token_user_a1 = self._get_token("usera1@ex.com", "12345678") + self.token_user_b1 = self._get_token("userb1@ex.com", "12345678") + self.token_super_a = self._get_token("supera@ex.com", "12345678") + + self.user_a1_meeting = Meetings.objects.create( + host_user=self.user_a1, + location="remote", + meeting_link="---", + date="2023-01-01 10:00:00", + ) + + self.user_a2_meeting = Meetings.objects.create( + host_user=self.user_a2, + location="remote", + meeting_link="---", + date="2023-01-01 10:00:00", + ) + + self.user_b1_meeting = Meetings.objects.create( + host_user=self.user_b1, + location="remote", + meeting_link="---", + date="2023-01-01 10:00:00", + ) + + self.user_a1_vacation = Vacation.objects.create( + applying_user=self.user_a1, type="vacations", status="approved", + reason="annual", from_date="2023-06-01 09:00:00", end_date="2023-06-03 18:00:00", actual_days=3, + ) + self.user_a2_vacation = Vacation.objects.create( + applying_user=self.user_a2, type="vacations", status="approved", + reason="annual", from_date="2023-06-05 09:00:00", end_date="2023-06-06 18:00:00", actual_days=2, + ) + self.user_b1_vacation = Vacation.objects.create( + applying_user=self.user_b1, type="vacations", status="approved", + reason="annual", from_date="2023-06-10 09:00:00", end_date="2023-06-12 18:00:00", actual_days=3, + ) + self.user_b2_vacation = Vacation.objects.create( + applying_user=self.user_b2, type="vacations", status="approved", + reason="annual", from_date="2023-06-15 09:00:00", end_date="2023-06-16 18:00:00", actual_days=2, + ) + + def _get_token(self, email, password): + url = "/api/auth/login/" + response = self.client.post( + url, {"email": email, "password": password}, format="json" + ) + return response.data["results"]["access_token"] + + def _auth(self, token): + self.client.credentials(HTTP_AUTHORIZATION="Bearer " + token) + + def test_admin_can_set_hidden_from(self): + """Admin can hide User A1 from User B1 and B2.""" + self._auth(self.token_admin_a) + url = f"/api/users/admin/{self.user_a1.id}/hidden_from/" + response = self.client.put( + url, {"hidden_from": [self.user_b1.id, self.user_b2.id]}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + hidden_ids = set(self.user_a1.hidden_from.values_list("id", flat=True)) + self.assertEqual(hidden_ids, {self.user_b1.id, self.user_b2.id}) + + def test_non_admin_cannot_set_hidden_from(self): + """Normal user cannot access the hidden_from endpoint.""" + self._auth(self.token_user_a1) + url = f"/api/users/admin/{self.user_a1.id}/hidden_from/" + response = self.client.put( + url, {"hidden_from": [self.user_b1.id]}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_supervisor_cannot_set_hidden_from(self): + """Supervisor cannot access the hidden_from endpoint.""" + self._auth(self.token_super_a) + url = f"/api/users/admin/{self.user_a1.id}/hidden_from/" + response = self.client.put( + url, {"hidden_from": [self.user_b1.id]}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_set_hidden_from_empty_list(self): + """Admin can clear hidden_from by sending empty list.""" + self._auth(self.token_admin_a) + url = f"/api/users/admin/{self.user_a1.id}/hidden_from/" + + response = self.client.put( + url, {"hidden_from": [self.user_b1.id]}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.user_a1.hidden_from.count(), 1) + + response = self.client.put(url, {"hidden_from": []}, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.user_a1.hidden_from.count(), 0) + + def test_set_hidden_from_invalid_user(self): + """Setting hidden_from with non-existent user ID just ignores it.""" + self._auth(self.token_admin_a) + url = f"/api/users/admin/{self.user_a1.id}/hidden_from/" + + response = self.client.put( + url, {"hidden_from": [self.user_b1.id]}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.user_a1.hidden_from.count(), 1) + + response = self.client.put(url, {"hidden_from": [9999]}, format="json") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(self.user_a1.hidden_from.count(), 1) + + def test_set_invalid_user_hidden_from(self): + """Setting hidden_from for non-existent user ID returns not found.""" + self._auth(self.token_admin_a) + url = f"/api/users/admin/{9999}/hidden_from/" + response = self.client.put( + url, {"hidden_from": [self.user_a1.id]}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(self.user_a1.hidden_from.count(), 0) + + def test_no_changes_returns_no_changes_message(self): + """Setting the same hidden_from list returns 'No changes'.""" + self._auth(self.token_admin_a) + url = f"/api/users/admin/{self.user_a1.id}/hidden_from/" + self.client.put(url, {"hidden_from": [self.user_b1.id]}, format="json") + response = self.client.put( + url, {"hidden_from": [self.user_b1.id]}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["message"], "No changes") + + def test_admin_can_get_hidden_from(self): + """Admin can get the hidden_from list.""" + self.user_a1.hidden_from.set([self.user_b1, self.user_b2]) + self._auth(self.token_admin_a) + url = f"/api/users/admin/{self.user_a1.id}/hidden_from/" + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + result_ids = {u["id"] for u in response.data["results"]} + self.assertEqual(result_ids, {self.user_b1.id, self.user_b2.id}) + + def test_get_hidden_from_empty(self): + """GET returns empty list when no users are hidden.""" + self._auth(self.token_admin_a) + url = f"/api/users/admin/{self.user_a1.id}/hidden_from/" + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["results"], []) + + def test_non_admin_cannot_get_hidden_from(self): + """Non-admin can not get the hidden_from list.""" + self._auth(self.token_user_a1) + url = f"/api/users/admin/{self.user_a1.id}/hidden_from/" + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_supervisor_cannot_get_hidden_from(self): + """Supervisor can not get the hidden_from list.""" + self._auth(self.token_super_a) + url = f"/api/users/admin/{self.user_a1.id}/hidden_from/" + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_hidden_user_not_in_user_list(self): + """User B1 should not see User A1 in user list when A1 is hidden from B1.""" + self.user_a1.hidden_from.add(self.user_b1, self.user_b2) + self._auth(self.token_user_b1) + url = "/api/users/" + response = self.client.get(url, format="json") + result_ids = {u["id"] for u in response.data["results"]} + self.assertNotIn(self.user_a1.id, result_ids) + + def test_non_hidden_user_still_visible(self): + """User B1 can still see User A2 when only A1 is hidden from B1.""" + self.user_a1.hidden_from.add(self.user_b1) + self._auth(self.token_user_b1) + url = "/api/users/" + response = self.client.get(url, format="json") + result_ids = {u["id"] for u in response.data["results"]} + self.assertIn(self.user_a2.id, result_ids) + + def test_hidden_user_visible_to_non_hidden_user(self): + """User A1 is hidden from B1 but still visible to A2.""" + self.user_a1.hidden_from.add(self.user_b1) + token_user_a2 = self._get_token("usera2@ex.com", "12345678") + self._auth(token_user_a2) + url = "/api/users/" + response = self.client.get(url, format="json") + result_ids = {u["id"] for u in response.data["results"]} + self.assertIn(self.user_a1.id, result_ids) + + def test_hidden_user_visible_to_admin(self): + """Admin can always see all users regardless of hidden_from.""" + self.user_a1.hidden_from.add(self.user_b1) + self._auth(self.token_admin_a) + url = "/api/users/admin/" + response = self.client.get(url, format="json") + result_ids = {u["id"] for u in response.data["results"]} + self.assertIn(self.user_a1.id, result_ids) + + def test_hidden_user_not_in_team_list(self): + """Hidden user should not appear in team member list.""" + self.user_a1.hidden_from.add(self.user_a2) + token_user_a2 = self._get_token("usera2@ex.com", "12345678") + self._auth(token_user_a2) + url = "/api/users/team/" + response = self.client.get(url, format="json") + result_ids = {u["id"] for u in response.data["results"]} + self.assertNotIn(self.user_a1.id, result_ids) + + def test_hidden_user_not_in_active_users(self): + """Hidden user should not appear in active users list.""" + self.user_a1.hidden_from.add(self.user_b1, self.user_b2) + self._auth(self.token_user_b1) + url = "/api/users/active/" + response = self.client.get(url, format="json") + result_ids = {u["id"] for u in response.data["results"]} + self.assertNotIn(self.user_a1.id, result_ids) + + def test_hidden_user_not_in_birthdates(self): + """Hidden user should not appear in birthdates list.""" + self.user_a1.hidden_from.add(self.user_b1, self.user_b2) + self._auth(self.token_user_b1) + url = "/api/users/birthdates/?month=3&day=10" # User A1 birthday is March 10 + response = self.client.get(url, format="json") + result_ids = {u["id"] for u in response.data["results"]} + self.assertNotIn(self.user_a1.id, result_ids) + + def test_meetings_hosted_by_hidden_user(self): + """meetings hosted by hidden user should not be visible""" + self.user_a1.hidden_from.add(self.user_b1) + self._auth(self.token_user_b1) + url = "/api/meeting/" + response = self.client.get(url, format="json") + result_ids = {u["id"] for u in response.data["results"]} + self.assertIn(self.user_a2_meeting.id, result_ids) + self.assertIn(self.user_b1_meeting.id, result_ids) + self.assertNotIn(self.user_a1_meeting.id, result_ids) + + def test_hidden_user_not_in_vacations(self): + """Hidden user should not appear in vacations list.""" + self.user_a1.hidden_from.add(self.user_b1, self.user_b2) + self._auth(self.token_user_b1) + url = "/api/vacations/" + response = self.client.get(url, format="json") + result_ids = {u["id"] for u in response.data["results"]} + self.assertIn(self.user_a2_vacation.id, result_ids) + self.assertIn(self.user_b1_vacation.id, result_ids) + self.assertNotIn(self.user_a1_vacation.id, result_ids) + + diff --git a/server/cshr/views/users.py b/server/cshr/views/users.py index f9d870932..d58fb1f0a 100644 --- a/server/cshr/views/users.py +++ b/server/cshr/views/users.py @@ -471,16 +471,22 @@ def put(self, request: Request, id: str) -> Response: if user is None: return CustomResponse.not_found(status_code=404, message="User not found") - new_hidden_from_ids = request.data.get("hidden_from") + new_hidden_from_ids = set(request.data.get("hidden_from")) current_ids = set(user.hidden_from.values_list("id", flat=True)) - if current_ids == set(new_hidden_from_ids): + if current_ids == new_hidden_from_ids: return CustomResponse.success( data=self.get_serializer(user.hidden_from.all(), many=True).data, message="No changes", status_code=200, ) - users = User.objects.filter(id__in=new_hidden_from_ids) + users = User.objects.filter(id__in=new_hidden_from_ids) # hidden from users list + if users.count() != len(new_hidden_from_ids): + # At least one of the hidden from users does not exist. + return CustomResponse.not_found( + status_code=404, + message="One of the hidden from users does not exist", + ) user.hidden_from.set(users) return CustomResponse.success( data=self.get_serializer(user.hidden_from.all(), many=True).data, From 71e64226945cd9a0539ccb6d935c1c8f9ee35af7 Mon Sep 17 00:00:00 2001 From: fatmaebrahim Date: Thu, 4 Jun 2026 20:27:55 +0300 Subject: [PATCH 3/6] feat: added hidden_from field to update user profile page Co-authored-by: Copilot --- .../src/components/dashboard/UpdateUser.vue | 13 +++++++++-- client/src/utils/add_update_user_form.ts | 23 +++++++++++++++++++ server/cshr/serializers/users.py | 10 ++++++++ server/cshr/views/users.py | 7 ++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/client/src/components/dashboard/UpdateUser.vue b/client/src/components/dashboard/UpdateUser.vue index 943a8a296..fcbf4b847 100644 --- a/client/src/components/dashboard/UpdateUser.vue +++ b/client/src/components/dashboard/UpdateUser.vue @@ -18,7 +18,7 @@ + :multiple="field.multiple" clearable>