From f8190daf68e0a7519c730b62442049db665fbd2e Mon Sep 17 00:00:00 2001 From: jbkyei1 Date: Thu, 2 Apr 2026 23:55:29 +0000 Subject: [PATCH 1/2] feat: Enhance skill service and technical task submission handling --- api/api_models/projects.py | 29 +- api/api_models/technical_task.py | 35 +- api/api_models/user.py | 3 + api/routes/project.py | 7 +- db/repository/org_chart.py | 38 +- db/repository/projects.py | 6 + db/repository/users.py | 24 +- scripts/url_mapping.json | 624 ---------------------- services/auth_service.py | 3 +- services/org_chart_service.py | 83 ++- services/project_service.py | 54 +- services/skill_service.py | 20 +- services/technical_task_service.py | 41 +- services/user_service.py | 4 + test/test_org_chart.py | 27 +- test/test_profile_page.py | 9 +- test/test_project_manager_validation.py | 166 ++++++ test/test_projects.py | 31 +- test/test_skill_service_response.py | 105 ++++ test/test_technical_task_submissions.py | 134 +++++ utils/email_templates/password-reset.html | 184 ++----- utils/email_templates/task_template.html | 147 +++-- utils/oauth2.py | 10 +- utils/permissions.py | 35 +- utils/utils.py | 9 +- 25 files changed, 904 insertions(+), 924 deletions(-) delete mode 100644 scripts/url_mapping.json create mode 100644 test/test_project_manager_validation.py create mode 100644 test/test_skill_service_response.py create mode 100644 test/test_technical_task_submissions.py diff --git a/api/api_models/projects.py b/api/api_models/projects.py index f52d09c..52485e9 100644 --- a/api/api_models/projects.py +++ b/api/api_models/projects.py @@ -21,11 +21,28 @@ class CreateProject(ProjectBase): project_tools: Optional[List[int]] = Field(None) +class MemberWithTeamResponse(BaseModel): + id: int + first_name: str = Field(...) + last_name: str = Field(...) + username: str = Field(...) + email: str = Field(...) + profile_pic_url: Optional[str] = None + team: Optional[str] = Field(None) + + model_config = ConfigDict(from_attributes=True) + + +# Alias for backward compatibility +MembersResponse = MemberWithTeamResponse + + class ProjectResponse(ProjectBase): id: int created_at: datetime updated_at: datetime - members: Optional[list[UserResponse]] = Field(None) + manager: Optional[UserResponse] = Field(None) + members: Optional[list[MemberWithTeamResponse]] = Field(None) stacks: Optional[list[Stacks]] = Field(None) project_tools: Optional[list[Skills]] = Field(None) status: ProjectStatus @@ -44,13 +61,3 @@ class UpdateProject(BaseModel): class ProjectMember(BaseModel): team: ProjectTeam = Field(...) - - -class MembersResponse(BaseModel): - id: int - first_name: str = Field(...) - last_name: str = Field(...) - username: str = Field(...) - email: str = Field(...) - profile_pic_url: Optional[str] = None - stack: Optional[Stacks] = Field(None) diff --git a/api/api_models/technical_task.py b/api/api_models/technical_task.py index d357297..b01aef7 100644 --- a/api/api_models/technical_task.py +++ b/api/api_models/technical_task.py @@ -22,9 +22,35 @@ class TechnicalTaskResponse(TechnicalTaskBase): class TechnicalTaskSubmissionBase(BaseModel): - github_link: Text - live_demo_url: Optional[Text] - description: Optional[Text] + github_link: Optional[Text] = None + live_demo_url: Optional[Text] = None + description: Optional[Text] = None + + +class UserMinimal(BaseModel): + id: int + first_name: str + last_name: str + username: str + profile_pic_url: Optional[str] = None + + model_config = ConfigDict(from_attributes=True) + + +class StackMinimal(BaseModel): + id: int + name: str + + model_config = ConfigDict(from_attributes=True) + + +class TechnicalTaskMinimal(BaseModel): + id: int + content: str + experience_level: ExperienceLevel + stack: Optional[StackMinimal] = None + + model_config = ConfigDict(from_attributes=True) class TechnicalTaskSubmissionResponse(TechnicalTaskSubmissionBase): @@ -32,5 +58,8 @@ class TechnicalTaskSubmissionResponse(TechnicalTaskSubmissionBase): created_at: datetime updated_at: datetime task_id: int + user_id: Optional[int] = None + user: Optional[UserMinimal] = None + technical_task: Optional[TechnicalTaskMinimal] = None model_config = ConfigDict(from_attributes=True) diff --git a/api/api_models/user.py b/api/api_models/user.py index f4d24a4..a697a91 100644 --- a/api/api_models/user.py +++ b/api/api_models/user.py @@ -139,6 +139,8 @@ class OrgChartNode(BaseModel): role: Optional[Role] = None stack: Optional[Stacks] = None manager_id: Optional[int] = None + status: str # Added for frontend filtering + is_active: bool # Added for frontend filtering subordinates: List[OrgChartNode] = [] model_config = ConfigDict(from_attributes=True) @@ -278,6 +280,7 @@ class Token(BaseModel): is_active: bool = Field(...) user_status: str = Field(...) refresh_token: str = Field(...) + role: Optional[Role] = Field(None) class TokenData(BaseModel): diff --git a/api/routes/project.py b/api/routes/project.py index 47a82c4..30bc38b 100644 --- a/api/routes/project.py +++ b/api/routes/project.py @@ -51,7 +51,12 @@ def get(project_id: int, db: Session = Depends(get_db)): @project_router.get("/", status_code=status.HTTP_200_OK, response_model=Page[ProjectResponse]) def get_all(db: Session = Depends(get_db)): - return paginate(db, _service(db).get_all_query()) + + page = paginate(db, _service(db).get_all_query()) + # Enrich members with team data + for project in page.items: + _service(db)._enrich_project_members_with_team(project) + return page @project_router.post("/{project_id}/add/{user_id}", status_code=status.HTTP_201_CREATED) diff --git a/db/repository/org_chart.py b/db/repository/org_chart.py index 411eb5c..72c0f73 100644 --- a/db/repository/org_chart.py +++ b/db/repository/org_chart.py @@ -13,12 +13,19 @@ class OrgChartRepository(BaseRepository): def get_user(self, user_id: int) -> Optional[User]: return self.db.query(User).filter(User.id == user_id).first() - def get_direct_subordinates(self, user_id: int) -> list[User]: - return ( - self.db.query(User) - .filter(User.manager_id == user_id) - .all() - ) + def get_direct_subordinates(self, user_id: int, filter_active: bool = False) -> list[User]: + """Get direct subordinates of a user. + + Args: + user_id: The manager's user ID + filter_active: If True, only return ACCEPTED + is_active users + """ + query = self.db.query(User).filter(User.manager_id == user_id) + + if filter_active: + query = query.filter(User.status == "ACCEPTED", User.is_active == True) + + return query.all() def get_subtree_ids(self, root_id: int, max_depth: int = 5) -> list[dict]: """Fetch all descendant user IDs using a recursive CTE, up to max_depth. @@ -58,13 +65,18 @@ def get_users_by_ids(self, user_ids: list[int]) -> list[User]: return [] return self.db.query(User).filter(User.id.in_(user_ids)).all() - def get_root_users(self) -> list[User]: - """Return users with no manager (org tree roots).""" - return ( - self.db.query(User) - .filter(User.manager_id.is_(None)) - .all() - ) + def get_root_users(self, filter_active: bool = False) -> list[User]: + """Return users with no manager (org tree roots). + + Args: + filter_active: If True, only return ACCEPTED + is_active users + """ + query = self.db.query(User).filter(User.manager_id.is_(None)) + + if filter_active: + query = query.filter(User.status == "ACCEPTED", User.is_active == True) + + return query.all() def get_ancestor_ids(self, user_id: int, max_depth: int = 50) -> list[int]: """Walk the manager chain upward using a recursive CTE. diff --git a/db/repository/projects.py b/db/repository/projects.py index 7c4855a..8b82786 100644 --- a/db/repository/projects.py +++ b/db/repository/projects.py @@ -30,6 +30,12 @@ def update(self, project: Project, update_data: dict) -> Project: def delete_with_memberships(self, project_id: int) -> None: self.db.query(UserProject).filter(UserProject.project_id == project_id).delete() + # Delete project_stacks entries + from db.models.project_stacks import ProjectStack + self.db.query(ProjectStack).filter(ProjectStack.project_id == project_id).delete() + # Delete project_skills entries + from db.models.project_skills import ProjectSkill + self.db.query(ProjectSkill).filter(ProjectSkill.project_id == project_id).delete() self.db.query(Project).filter(Project.id == project_id).delete() self.db.commit() diff --git a/db/repository/users.py b/db/repository/users.py index 399b729..bdd7dd1 100644 --- a/db/repository/users.py +++ b/db/repository/users.py @@ -20,8 +20,18 @@ def get_by_username(self, username: str) -> Optional[User]: return self.db.query(User).filter(User.username == username).first() def create(self, user_data: UserSignUp) -> User: + from db.models.roles import Role + from utils.utils import RoleChoices + data = user_data.model_dump().copy() data.pop("password_confirmation") + + # Ensure role_id is set - default to USER role if not provided + if not data.get("role_id"): + user_role = self.db.query(Role).filter(Role.name == RoleChoices.USER).first() + if user_role: + data["role_id"] = user_role.id + new_user = User(**data) return self.save(new_user) @@ -56,6 +66,8 @@ def update_avatar(self, user: User, url: str) -> User: def build_search_query(self, skill: Optional[str], stack: Optional[str], active: Optional[bool], p: Optional[str]): + from sqlalchemy import or_ + query = select(User).order_by(desc(User.created_at)) if skill: query = query.join(users_skills.UserSkill).join(Skill).filter( @@ -64,7 +76,17 @@ def build_search_query(self, skill: Optional[str], stack: Optional[str], if stack: query = query.filter(User.stack.has(name=stack.capitalize())) if active is not None: - query = query.filter(User.is_active == active) + if active: # active=True means Directory + # Only show ACCEPTED + is_active users + query = query.filter(User.is_active.is_(True), User.status == "ACCEPTED") + else: # active=False means Applicants + # Show: (is_active=false any status) OR (is_active=true but status != ACCEPTED) + query = query.filter( + or_( + User.is_active.is_(False), + User.is_active.is_(True) & (User.status != "ACCEPTED") + ) + ) if p: p_escaped = p.replace("%", r"\%").replace("_", r"\_") query = query.filter( diff --git a/scripts/url_mapping.json b/scripts/url_mapping.json deleted file mode 100644 index 247b0d6..0000000 --- a/scripts/url_mapping.json +++ /dev/null @@ -1,624 +0,0 @@ -{ - "AWS/skills/20240803-20-03-57/AWS": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859378/AWS/skills/20240803-20-03-57/AWS.png", - "AWS/skills/20240803-20-06-47/AWS": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859378/AWS/skills/20240803-20-06-47/AWS.png", - "AWS/skills/20240803-20-45-22/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859379/AWS/skills/20240803-20-45-22/AWS.png", - "AWS/skills/20240803-20-47-47/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859381/AWS/skills/20240803-20-47-47/AWS.png", - "AWS/skills/20240803-21-22-04/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859382/AWS/skills/20240803-21-22-04/AWS.png", - "AWS/skills/20240803-21-27-40/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859383/AWS/skills/20240803-21-27-40/AWS.png", - "AWS/skills/20240805-14-59-51/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859384/AWS/skills/20240805-14-59-51/AWS.png", - "AWS/skills/20240818-21-07-45/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859384/AWS/skills/20240818-21-07-45/AWS.png", - "AWS/skills/20240830-19-44-41/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859385/AWS/skills/20240830-19-44-41/AWS.png", - "Android/skills/20240803-19-48-30/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859386/Android/skills/20240803-19-48-30/Android.png", - "Android/skills/20240803-19-50-17/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859386/Android/skills/20240803-19-50-17/Android.png", - "Android/skills/20240803-19-51-07/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859387/Android/skills/20240803-19-51-07/Android.png", - "Android/skills/20240803-19-51-47/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859388/Android/skills/20240803-19-51-47/Android.png", - "Android/skills/20240803-19-53-00/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859389/Android/skills/20240803-19-53-00/Android.png", - "Android/skills/20240803-19-53-27/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859390/Android/skills/20240803-19-53-27/Android.png", - "Android/skills/20240803-19-54-26/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859391/Android/skills/20240803-19-54-26/Android.png", - "Android/skills/20240803-19-56-10/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859391/Android/skills/20240803-19-56-10/Android.png", - "Android/skills/20240803-19-56-32/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859392/Android/skills/20240803-19-56-32/Android.png", - "Android/skills/20240803-19-57-51/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859393/Android/skills/20240803-19-57-51/Android.png", - "Android/skills/20240803-19-58-51/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859393/Android/skills/20240803-19-58-51/Android.png", - "Android/skills/20240803-20-03-16/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859394/Android/skills/20240803-20-03-16/Android.png", - "Android/skills/20240803-20-03-52/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859394/Android/skills/20240803-20-03-52/Android.png", - "Android/skills/20240803-20-06-42/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859395/Android/skills/20240803-20-06-42/Android.png", - "Android/skills/20240803-20-45-16/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859397/Android/skills/20240803-20-45-16/Android.png", - "Android/skills/20240803-20-47-42/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859398/Android/skills/20240803-20-47-42/Android.png", - "Android/skills/20240803-21-22-03/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859399/Android/skills/20240803-21-22-03/Android.png", - "Android/skills/20240803-21-27-33/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859400/Android/skills/20240803-21-27-33/Android.png", - "Android/skills/20240805-14-59-44/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859401/Android/skills/20240805-14-59-44/Android.png", - "Android/skills/20240818-21-07-35/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859402/Android/skills/20240818-21-07-35/Android.png", - "Android/skills/20240830-19-44-12/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859402/Android/skills/20240830-19-44-12/Android.png", - "Angular/skills/20240803-19-48-31/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859403/Angular/skills/20240803-19-48-31/Angular.png", - "Angular/skills/20240803-19-50-19/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859404/Angular/skills/20240803-19-50-19/Angular.png", - "Angular/skills/20240803-19-51-08/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859404/Angular/skills/20240803-19-51-08/Angular.png", - "Angular/skills/20240803-19-51-49/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859405/Angular/skills/20240803-19-51-49/Angular.png", - "Angular/skills/20240803-19-53-01/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859405/Angular/skills/20240803-19-53-01/Angular.png", - "Angular/skills/20240803-19-53-28/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859406/Angular/skills/20240803-19-53-28/Angular.png", - "Angular/skills/20240803-19-54-27/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859407/Angular/skills/20240803-19-54-27/Angular.png", - "Angular/skills/20240803-19-56-11/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859408/Angular/skills/20240803-19-56-11/Angular.png", - "Angular/skills/20240803-19-56-33/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859408/Angular/skills/20240803-19-56-33/Angular.png", - "Angular/skills/20240803-19-57-52/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859409/Angular/skills/20240803-19-57-52/Angular.png", - "Angular/skills/20240803-19-58-53/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859410/Angular/skills/20240803-19-58-53/Angular.png", - "Angular/skills/20240803-20-03-17/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859411/Angular/skills/20240803-20-03-17/Angular.png", - "Angular/skills/20240803-20-03-53/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859411/Angular/skills/20240803-20-03-53/Angular.png", - "Angular/skills/20240803-20-06-43/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859412/Angular/skills/20240803-20-06-43/Angular.png", - "Angular/skills/20240803-20-45-18/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859413/Angular/skills/20240803-20-45-18/Angular.png", - "Angular/skills/20240803-20-47-43/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859413/Angular/skills/20240803-20-47-43/Angular.png", - "Angular/skills/20240803-21-22-03/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859414/Angular/skills/20240803-21-22-03/Angular.png", - "Angular/skills/20240803-21-27-34/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859415/Angular/skills/20240803-21-27-34/Angular.png", - "Angular/skills/20240805-14-59-46/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859416/Angular/skills/20240805-14-59-46/Angular.png", - "Angular/skills/20240818-21-07-38/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859417/Angular/skills/20240818-21-07-38/Angular.png", - "Angular/skills/20240830-19-44-19/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859418/Angular/skills/20240830-19-44-19/Angular.png", - "Ansible/skills/20240803-20-03-54/Ansible": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859420/Ansible/skills/20240803-20-03-54/Ansible.png", - "Ansible/skills/20240803-20-06-45/Ansible": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859421/Ansible/skills/20240803-20-06-45/Ansible.png", - "Ansible/skills/20240803-20-45-19/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859421/Ansible/skills/20240803-20-45-19/Ansible.png", - "Ansible/skills/20240803-20-47-44/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859422/Ansible/skills/20240803-20-47-44/Ansible.png", - "Ansible/skills/20240803-21-22-04/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859423/Ansible/skills/20240803-21-22-04/Ansible.png", - "Ansible/skills/20240803-21-27-36/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859424/Ansible/skills/20240803-21-27-36/Ansible.png", - "Ansible/skills/20240805-14-59-48/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859424/Ansible/skills/20240805-14-59-48/Ansible.png", - "Ansible/skills/20240818-21-07-41/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859425/Ansible/skills/20240818-21-07-41/Ansible.png", - "Ansible/skills/20240830-19-44-27/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859426/Ansible/skills/20240830-19-44-27/Ansible.png", - "Apache/skills/20240803-20-03-56/Apache": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859427/Apache/skills/20240803-20-03-56/Apache.png", - "Apache/skills/20240803-20-06-46/Apache": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859427/Apache/skills/20240803-20-06-46/Apache.png", - "Apache/skills/20240803-20-45-21/Apache.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859428/Apache/skills/20240803-20-45-21/Apache.png", - "Apache/skills/20240803-20-47-46/Apache.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859429/Apache/skills/20240803-20-47-46/Apache.png", - "Apache/skills/20240803-21-22-04/Apache.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859429/Apache/skills/20240803-21-22-04/Apache.png", - "Apache/skills/20240803-21-27-38/Apache.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859430/Apache/skills/20240803-21-27-38/Apache.png", - "Apache/skills/20240805-14-59-50/Apache.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859431/Apache/skills/20240805-14-59-50/Apache.png", - "Apache/skills/20240818-21-07-43/Apache.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859432/Apache/skills/20240818-21-07-43/Apache.png", - "Apache/skills/20240830-19-44-33/Apache.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859432/Apache/skills/20240830-19-44-33/Apache.png", - "Bitbucket/skills/20240803-20-03-58/Bitbucket": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859433/Bitbucket/skills/20240803-20-03-58/Bitbucket.png", - "Bitbucket/skills/20240803-20-06-48/Bitbucket": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859434/Bitbucket/skills/20240803-20-06-48/Bitbucket.png", - "Bitbucket/skills/20240803-20-45-24/Bitbucket.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859436/Bitbucket/skills/20240803-20-45-24/Bitbucket.png", - "Bitbucket/skills/20240803-20-47-49/Bitbucket.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859436/Bitbucket/skills/20240803-20-47-49/Bitbucket.png", - "Bitbucket/skills/20240803-21-22-05/Bitbucket.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859437/Bitbucket/skills/20240803-21-22-05/Bitbucket.png", - "Bitbucket/skills/20240803-21-27-42/Bitbucket.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859438/Bitbucket/skills/20240803-21-27-42/Bitbucket.png", - "Bitbucket/skills/20240805-14-59-54/Bitbucket.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859439/Bitbucket/skills/20240805-14-59-54/Bitbucket.png", - "Bitbucket/skills/20240818-21-07-46/Bitbucket.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859440/Bitbucket/skills/20240818-21-07-46/Bitbucket.png", - "Bitbucket/skills/20240830-19-44-47/Bitbucket.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859441/Bitbucket/skills/20240830-19-44-47/Bitbucket.png", - "Bootstrap/skills/20240803-20-04-00/Bootstrap": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859441/Bootstrap/skills/20240803-20-04-00/Bootstrap.png", - "Bootstrap/skills/20240803-20-06-50/Bootstrap": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859442/Bootstrap/skills/20240803-20-06-50/Bootstrap.png", - "Bootstrap/skills/20240803-20-45-25/Bootstrap.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859443/Bootstrap/skills/20240803-20-45-25/Bootstrap.png", - "Bootstrap/skills/20240803-20-47-50/Bootstrap.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859443/Bootstrap/skills/20240803-20-47-50/Bootstrap.png", - "Bootstrap/skills/20240803-21-22-05/Bootstrap.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859444/Bootstrap/skills/20240803-21-22-05/Bootstrap.png", - "Bootstrap/skills/20240803-21-27-44/Bootstrap.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859445/Bootstrap/skills/20240803-21-27-44/Bootstrap.png", - "Bootstrap/skills/20240805-14-59-55/Bootstrap.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859445/Bootstrap/skills/20240805-14-59-55/Bootstrap.png", - "Bootstrap/skills/20240818-21-07-48/Bootstrap.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859446/Bootstrap/skills/20240818-21-07-48/Bootstrap.png", - "Bootstrap/skills/20240830-19-44-53/Bootstrap.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859447/Bootstrap/skills/20240830-19-44-53/Bootstrap.png", - "Cassandra/skills/20240803-20-04-01/Cassandra": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859448/Cassandra/skills/20240803-20-04-01/Cassandra.png", - "Cassandra/skills/20240803-20-06-51/Cassandra": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859448/Cassandra/skills/20240803-20-06-51/Cassandra.png", - "Cassandra/skills/20240803-20-45-27/Cassandra.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859449/Cassandra/skills/20240803-20-45-27/Cassandra.png", - "Cassandra/skills/20240803-20-47-52/Cassandra.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859449/Cassandra/skills/20240803-20-47-52/Cassandra.png", - "Cassandra/skills/20240803-21-22-06/Cassandra.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859450/Cassandra/skills/20240803-21-22-06/Cassandra.png", - "Cassandra/skills/20240803-21-27-46/Cassandra.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859451/Cassandra/skills/20240803-21-27-46/Cassandra.png", - "Cassandra/skills/20240805-14-59-57/Cassandra.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859451/Cassandra/skills/20240805-14-59-57/Cassandra.png", - "Cassandra/skills/20240818-21-07-51/Cassandra.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859452/Cassandra/skills/20240818-21-07-51/Cassandra.png", - "Cassandra/skills/20240830-19-45-01/Cassandra.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859453/Cassandra/skills/20240830-19-45-01/Cassandra.png", - "Chef/skills/20240803-20-04-03/Chef": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859454/Chef/skills/20240803-20-04-03/Chef.png", - "Chef/skills/20240803-20-06-52/Chef": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859454/Chef/skills/20240803-20-06-52/Chef.png", - "Chef/skills/20240803-20-45-28/Chef.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859455/Chef/skills/20240803-20-45-28/Chef.png", - "Chef/skills/20240803-20-47-53/Chef.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859456/Chef/skills/20240803-20-47-53/Chef.png", - "Chef/skills/20240803-21-22-06/Chef.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859456/Chef/skills/20240803-21-22-06/Chef.png", - "Chef/skills/20240803-21-27-47/Chef.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859457/Chef/skills/20240803-21-27-47/Chef.png", - "Chef/skills/20240805-14-59-59/Chef.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859460/Chef/skills/20240805-14-59-59/Chef.png", - "Chef/skills/20240818-21-07-57/Chef.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859461/Chef/skills/20240818-21-07-57/Chef.png", - "Chef/skills/20240830-19-45-07/Chef.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859462/Chef/skills/20240830-19-45-07/Chef.png", - "Dart/skills/20240803-20-04-04/Dart": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859463/Dart/skills/20240803-20-04-04/Dart.png", - "Dart/skills/20240803-20-06-54/Dart": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859464/Dart/skills/20240803-20-06-54/Dart.png", - "Dart/skills/20240803-20-45-30/Dart.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859464/Dart/skills/20240803-20-45-30/Dart.png", - "Dart/skills/20240803-20-47-55/Dart.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859465/Dart/skills/20240803-20-47-55/Dart.png", - "Dart/skills/20240803-21-22-06/Dart.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859466/Dart/skills/20240803-21-22-06/Dart.png", - "Dart/skills/20240803-21-27-49/Dart.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859467/Dart/skills/20240803-21-27-49/Dart.png", - "Dart/skills/20240805-15-00-01/Dart.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859468/Dart/skills/20240805-15-00-01/Dart.png", - "Dart/skills/20240818-21-08-00/Dart.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859469/Dart/skills/20240818-21-08-00/Dart.png", - "Dart/skills/20240830-19-45-16/Dart.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859470/Dart/skills/20240830-19-45-16/Dart.png", - "Django/skills/20240803-20-04-06/Django": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859470/Django/skills/20240803-20-04-06/Django.png", - "Django/skills/20240803-20-06-55/Django": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859471/Django/skills/20240803-20-06-55/Django.png", - "Django/skills/20240803-20-45-31/Django.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859472/Django/skills/20240803-20-45-31/Django.png", - "Django/skills/20240803-20-47-57/Django.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859472/Django/skills/20240803-20-47-57/Django.png", - "Django/skills/20240803-21-22-07/Django.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859473/Django/skills/20240803-21-22-07/Django.png", - "Django/skills/20240803-21-27-51/Django.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859474/Django/skills/20240803-21-27-51/Django.png", - "Django/skills/20240805-15-00-03/Django.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859474/Django/skills/20240805-15-00-03/Django.png", - "Django/skills/20240818-21-08-17/Django.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859475/Django/skills/20240818-21-08-17/Django.png", - "Django/skills/20240830-19-45-25/Django.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859476/Django/skills/20240830-19-45-25/Django.png", - "Django-E-Commerce-Dockerise/.git/HEAD": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859477/Django-E-Commerce-Dockerise/.git/HEAD", - "Django-E-Commerce-Dockerise/.git/config": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859479/Django-E-Commerce-Dockerise/.git/config", - "Django-E-Commerce-Dockerise/.git/description": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859480/Django-E-Commerce-Dockerise/.git/description", - "Django-E-Commerce-Dockerise/.git/hooks/applypatch-msg.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859480/Django-E-Commerce-Dockerise/.git/hooks/applypatch-msg.sample", - "Django-E-Commerce-Dockerise/.git/hooks/commit-msg.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859481/Django-E-Commerce-Dockerise/.git/hooks/commit-msg.sample", - "Django-E-Commerce-Dockerise/.git/hooks/fsmonitor-watchman.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859482/Django-E-Commerce-Dockerise/.git/hooks/fsmonitor-watchman.sample", - "Django-E-Commerce-Dockerise/.git/hooks/post-update.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859483/Django-E-Commerce-Dockerise/.git/hooks/post-update.sample", - "Django-E-Commerce-Dockerise/.git/hooks/pre-applypatch.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859484/Django-E-Commerce-Dockerise/.git/hooks/pre-applypatch.sample", - "Django-E-Commerce-Dockerise/.git/hooks/pre-commit.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859484/Django-E-Commerce-Dockerise/.git/hooks/pre-commit.sample", - "Django-E-Commerce-Dockerise/.git/hooks/pre-merge-commit.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859485/Django-E-Commerce-Dockerise/.git/hooks/pre-merge-commit.sample", - "Django-E-Commerce-Dockerise/.git/hooks/pre-push.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859485/Django-E-Commerce-Dockerise/.git/hooks/pre-push.sample", - "Django-E-Commerce-Dockerise/.git/hooks/pre-rebase.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859486/Django-E-Commerce-Dockerise/.git/hooks/pre-rebase.sample", - "Django-E-Commerce-Dockerise/.git/hooks/pre-receive.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859486/Django-E-Commerce-Dockerise/.git/hooks/pre-receive.sample", - "Django-E-Commerce-Dockerise/.git/hooks/prepare-commit-msg.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859487/Django-E-Commerce-Dockerise/.git/hooks/prepare-commit-msg.sample", - "Django-E-Commerce-Dockerise/.git/hooks/push-to-checkout.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859488/Django-E-Commerce-Dockerise/.git/hooks/push-to-checkout.sample", - "Django-E-Commerce-Dockerise/.git/hooks/update.sample": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859488/Django-E-Commerce-Dockerise/.git/hooks/update.sample", - "Django-E-Commerce-Dockerise/.git/index": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859489/Django-E-Commerce-Dockerise/.git/index", - "Django-E-Commerce-Dockerise/.git/info/exclude": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859490/Django-E-Commerce-Dockerise/.git/info/exclude", - "Django-E-Commerce-Dockerise/.git/logs/HEAD": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859491/Django-E-Commerce-Dockerise/.git/logs/HEAD", - "Django-E-Commerce-Dockerise/.git/logs/refs/heads/master": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859491/Django-E-Commerce-Dockerise/.git/logs/refs/heads/master", - "Django-E-Commerce-Dockerise/.git/logs/refs/remotes/origin/HEAD": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859492/Django-E-Commerce-Dockerise/.git/logs/refs/remotes/origin/HEAD", - "Django-E-Commerce-Dockerise/.git/objects/pack/pack-1fef77ede18e8bf811f600001b7822c39e9ac654.idx": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859493/Django-E-Commerce-Dockerise/.git/objects/pack/pack-1fef77ede18e8bf811f600001b7822c39e9ac654.idx", - "Django-E-Commerce-Dockerise/.git/objects/pack/pack-1fef77ede18e8bf811f600001b7822c39e9ac654.pack": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859501/Django-E-Commerce-Dockerise/.git/objects/pack/pack-1fef77ede18e8bf811f600001b7822c39e9ac654.pack", - "Django-E-Commerce-Dockerise/.git/packed-refs": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859502/Django-E-Commerce-Dockerise/.git/packed-refs", - "Django-E-Commerce-Dockerise/.git/refs/heads/master": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859502/Django-E-Commerce-Dockerise/.git/refs/heads/master", - "Django-E-Commerce-Dockerise/.git/refs/remotes/origin/HEAD": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859503/Django-E-Commerce-Dockerise/.git/refs/remotes/origin/HEAD", - "Django-E-Commerce-Dockerise/.gitignore": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859504/Django-E-Commerce-Dockerise/.gitignore", - "Django-E-Commerce-Dockerise/Dockerfile": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859504/Django-E-Commerce-Dockerise/Dockerfile", - "Django-E-Commerce-Dockerise/LICENSE": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859505/Django-E-Commerce-Dockerise/LICENSE", - "Django-E-Commerce-Dockerise/README.md": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859505/Django-E-Commerce-Dockerise/README.md", - "Django-E-Commerce-Dockerise/babyshop_app/babyshop/__init__.py": "ERROR: Empty file", - "Django-E-Commerce-Dockerise/babyshop_app/babyshop/asgi.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859506/Django-E-Commerce-Dockerise/babyshop_app/babyshop/asgi.py", - "Django-E-Commerce-Dockerise/babyshop_app/babyshop/settings.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859507/Django-E-Commerce-Dockerise/babyshop_app/babyshop/settings.py", - "Django-E-Commerce-Dockerise/babyshop_app/babyshop/urls.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859509/Django-E-Commerce-Dockerise/babyshop_app/babyshop/urls.py", - "Django-E-Commerce-Dockerise/babyshop_app/babyshop/wsgi.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859509/Django-E-Commerce-Dockerise/babyshop_app/babyshop/wsgi.py", - "Django-E-Commerce-Dockerise/babyshop_app/manage.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859510/Django-E-Commerce-Dockerise/babyshop_app/manage.py", - "Django-E-Commerce-Dockerise/babyshop_app/products/__init__.py": "ERROR: Empty file", - "Django-E-Commerce-Dockerise/babyshop_app/products/admin.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859511/Django-E-Commerce-Dockerise/babyshop_app/products/admin.py", - "Django-E-Commerce-Dockerise/babyshop_app/products/apps.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859512/Django-E-Commerce-Dockerise/babyshop_app/products/apps.py", - "Django-E-Commerce-Dockerise/babyshop_app/products/migrations/0001_initial.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859512/Django-E-Commerce-Dockerise/babyshop_app/products/migrations/0001_initial.py", - "Django-E-Commerce-Dockerise/babyshop_app/products/migrations/0002_product_price.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859515/Django-E-Commerce-Dockerise/babyshop_app/products/migrations/0002_product_price.py", - "Django-E-Commerce-Dockerise/babyshop_app/products/migrations/0003_alter_product_name.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859515/Django-E-Commerce-Dockerise/babyshop_app/products/migrations/0003_alter_product_name.py", - "Django-E-Commerce-Dockerise/babyshop_app/products/migrations/0004_category_product_category.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859516/Django-E-Commerce-Dockerise/babyshop_app/products/migrations/0004_category_product_category.py", - "Django-E-Commerce-Dockerise/babyshop_app/products/migrations/0005_rename_describtion_product_description.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859517/Django-E-Commerce-Dockerise/babyshop_app/products/migrations/0005_rename_describtion_product_description.py", - "Django-E-Commerce-Dockerise/babyshop_app/products/migrations/__init__.py": "ERROR: Empty file", - "Django-E-Commerce-Dockerise/babyshop_app/products/models.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859518/Django-E-Commerce-Dockerise/babyshop_app/products/models.py", - "Django-E-Commerce-Dockerise/babyshop_app/products/tests.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859518/Django-E-Commerce-Dockerise/babyshop_app/products/tests.py", - "Django-E-Commerce-Dockerise/babyshop_app/products/urls.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859519/Django-E-Commerce-Dockerise/babyshop_app/products/urls.py", - "Django-E-Commerce-Dockerise/babyshop_app/products/views.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859519/Django-E-Commerce-Dockerise/babyshop_app/products/views.py", - "Django-E-Commerce-Dockerise/babyshop_app/templates/login.html": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859520/Django-E-Commerce-Dockerise/babyshop_app/templates/login.html", - "Django-E-Commerce-Dockerise/babyshop_app/templates/partoftemp/_dashboard.html": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859521/Django-E-Commerce-Dockerise/babyshop_app/templates/partoftemp/_dashboard.html", - "Django-E-Commerce-Dockerise/babyshop_app/templates/partoftemp/footer.html": "ERROR: Empty file", - "Django-E-Commerce-Dockerise/babyshop_app/templates/product.html": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859521/Django-E-Commerce-Dockerise/babyshop_app/templates/product.html", - "Django-E-Commerce-Dockerise/babyshop_app/templates/products.html": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859522/Django-E-Commerce-Dockerise/babyshop_app/templates/products.html", - "Django-E-Commerce-Dockerise/babyshop_app/templates/register.html": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859523/Django-E-Commerce-Dockerise/babyshop_app/templates/register.html", - "Django-E-Commerce-Dockerise/babyshop_app/users/__init__.py": "ERROR: Empty file", - "Django-E-Commerce-Dockerise/babyshop_app/users/admin.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859525/Django-E-Commerce-Dockerise/babyshop_app/users/admin.py", - "Django-E-Commerce-Dockerise/babyshop_app/users/apps.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859525/Django-E-Commerce-Dockerise/babyshop_app/users/apps.py", - "Django-E-Commerce-Dockerise/babyshop_app/users/forms.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859526/Django-E-Commerce-Dockerise/babyshop_app/users/forms.py", - "Django-E-Commerce-Dockerise/babyshop_app/users/migrations/__init__.py": "ERROR: Empty file", - "Django-E-Commerce-Dockerise/babyshop_app/users/models.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859527/Django-E-Commerce-Dockerise/babyshop_app/users/models.py", - "Django-E-Commerce-Dockerise/babyshop_app/users/tests.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859528/Django-E-Commerce-Dockerise/babyshop_app/users/tests.py", - "Django-E-Commerce-Dockerise/babyshop_app/users/urls.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859528/Django-E-Commerce-Dockerise/babyshop_app/users/urls.py", - "Django-E-Commerce-Dockerise/babyshop_app/users/views.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859529/Django-E-Commerce-Dockerise/babyshop_app/users/views.py", - "Django-E-Commerce-Dockerise/project_images/capture_20220323080815407.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859541/Django-E-Commerce-Dockerise/project_images/capture_20220323080815407.bmp", - "Django-E-Commerce-Dockerise/project_images/capture_20220323080840305.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859544/Django-E-Commerce-Dockerise/project_images/capture_20220323080840305.bmp", - "Django-E-Commerce-Dockerise/project_images/capture_20220323080934541.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859547/Django-E-Commerce-Dockerise/project_images/capture_20220323080934541.bmp", - "Django-E-Commerce-Dockerise/project_images/capture_20220323080953570.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859551/Django-E-Commerce-Dockerise/project_images/capture_20220323080953570.bmp", - "Django-E-Commerce-Dockerise/project_images/capture_20220323081016022.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859555/Django-E-Commerce-Dockerise/project_images/capture_20220323081016022.bmp", - "Django-E-Commerce-Dockerise/project_images/capture_20220323081044867.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859559/Django-E-Commerce-Dockerise/project_images/capture_20220323081044867.bmp", - "Docker/skills/20240803-20-04-07/Docker": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859560/Docker/skills/20240803-20-04-07/Docker.png", - "Docker/skills/20240803-20-06-56/Docker": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859561/Docker/skills/20240803-20-06-56/Docker.png", - "Docker/skills/20240803-20-45-32/Docker.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859562/Docker/skills/20240803-20-45-32/Docker.png", - "Docker/skills/20240803-20-47-59/Docker.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859563/Docker/skills/20240803-20-47-59/Docker.png", - "Docker/skills/20240803-21-22-07/Docker.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859565/Docker/skills/20240803-21-22-07/Docker.png", - "Docker/skills/20240803-21-27-52/Docker.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859566/Docker/skills/20240803-21-27-52/Docker.png", - "Docker/skills/20240805-15-00-04/Docker.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859567/Docker/skills/20240805-15-00-04/Docker.png", - "Docker/skills/20240818-21-08-19/Docker.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859568/Docker/skills/20240818-21-08-19/Docker.png", - "Docker/skills/20240830-19-45-30/Docker.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859573/Docker/skills/20240830-19-45-30/Docker.png", - "FastAPI/skills/20240803-20-04-09/FastAPI": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859574/FastAPI/skills/20240803-20-04-09/FastAPI.png", - "FastAPI/skills/20240803-20-06-58/FastAPI": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859575/FastAPI/skills/20240803-20-06-58/FastAPI.png", - "FastAPI/skills/20240803-20-45-34/FastAPI.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859575/FastAPI/skills/20240803-20-45-34/FastAPI.png", - "FastAPI/skills/20240803-20-48-00/FastAPI.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859576/FastAPI/skills/20240803-20-48-00/FastAPI.png", - "FastAPI/skills/20240803-21-22-08/FastAPI.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859577/FastAPI/skills/20240803-21-22-08/FastAPI.png", - "FastAPI/skills/20240803-21-27-54/FastAPI.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859578/FastAPI/skills/20240803-21-27-54/FastAPI.png", - "FastAPI/skills/20240805-15-00-07/FastAPI.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859578/FastAPI/skills/20240805-15-00-07/FastAPI.png", - "FastAPI/skills/20240818-21-08-23/FastAPI.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859579/FastAPI/skills/20240818-21-08-23/FastAPI.png", - "FastAPI/skills/20240830-19-45-38/FastAPI.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859579/FastAPI/skills/20240830-19-45-38/FastAPI.png", - "Flask/skills/20240803-20-04-12/Flask": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859581/Flask/skills/20240803-20-04-12/Flask.png", - "Flask/skills/20240803-20-07-00/Flask": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859582/Flask/skills/20240803-20-07-00/Flask.png", - "Flask/skills/20240803-20-45-36/Flask.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859583/Flask/skills/20240803-20-45-36/Flask.png", - "Flask/skills/20240803-20-48-02/Flask.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859585/Flask/skills/20240803-20-48-02/Flask.png", - "Flask/skills/20240803-21-22-08/Flask.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859585/Flask/skills/20240803-21-22-08/Flask.png", - "Flask/skills/20240803-21-27-56/Flask.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859586/Flask/skills/20240803-21-27-56/Flask.png", - "Flask/skills/20240805-15-00-09/Flask.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859587/Flask/skills/20240805-15-00-09/Flask.png", - "Flask/skills/20240818-21-08-29/Flask.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859588/Flask/skills/20240818-21-08-29/Flask.png", - "Flask/skills/20240830-19-45-45/Flask.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859588/Flask/skills/20240830-19-45-45/Flask.png", - "Flutter/skills/20240803-20-04-13/Flutter": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859589/Flutter/skills/20240803-20-04-13/Flutter.png", - "Flutter/skills/20240803-20-07-01/Flutter": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859589/Flutter/skills/20240803-20-07-01/Flutter.png", - "Flutter/skills/20240803-20-45-38/Flutter.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859590/Flutter/skills/20240803-20-45-38/Flutter.png", - "Flutter/skills/20240803-20-48-03/Flutter.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859591/Flutter/skills/20240803-20-48-03/Flutter.png", - "Flutter/skills/20240803-21-22-08/Flutter.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859592/Flutter/skills/20240803-21-22-08/Flutter.png", - "Flutter/skills/20240803-21-27-57/Flutter.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859593/Flutter/skills/20240803-21-27-57/Flutter.png", - "Flutter/skills/20240805-15-00-11/Flutter.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859594/Flutter/skills/20240805-15-00-11/Flutter.png", - "Flutter/skills/20240818-21-08-31/Flutter.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859594/Flutter/skills/20240818-21-08-31/Flutter.png", - "Flutter/skills/20240830-19-45-55/Flutter.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859595/Flutter/skills/20240830-19-45-55/Flutter.png", - "GitLab/skills/20240803-20-04-15/GitLab": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859596/GitLab/skills/20240803-20-04-15/GitLab.png", - "GitLab/skills/20240803-20-07-02/GitLab": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859597/GitLab/skills/20240803-20-07-02/GitLab.png", - "GitLab/skills/20240803-20-45-40/GitLab.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859597/GitLab/skills/20240803-20-45-40/GitLab.png", - "GitLab/skills/20240803-20-48-04/GitLab.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859598/GitLab/skills/20240803-20-48-04/GitLab.png", - "GitLab/skills/20240803-21-22-09/GitLab.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859598/GitLab/skills/20240803-21-22-09/GitLab.png", - "GitLab/skills/20240803-21-27-59/GitLab.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859599/GitLab/skills/20240803-21-27-59/GitLab.png", - "GitLab/skills/20240805-15-00-13/GitLab.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859600/GitLab/skills/20240805-15-00-13/GitLab.png", - "GitLab/skills/20240818-21-08-33/GitLab.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859600/GitLab/skills/20240818-21-08-33/GitLab.png", - "GitLab/skills/20240830-19-46-03/GitLab.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859601/GitLab/skills/20240830-19-46-03/GitLab.png", - "Go/skills/20240803-20-04-16/Go": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859603/Go/skills/20240803-20-04-16/Go.png", - "Go/skills/20240803-20-07-03/Go": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859603/Go/skills/20240803-20-07-03/Go.png", - "Go/skills/20240803-20-48-05/Go.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859604/Go/skills/20240803-20-48-05/Go.png", - "Go/skills/20240803-21-22-09/Go.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859605/Go/skills/20240803-21-22-09/Go.png", - "Go/skills/20240803-21-28-00/Go.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859606/Go/skills/20240803-21-28-00/Go.png", - "Go/skills/20240805-15-00-15/Go.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859607/Go/skills/20240805-15-00-15/Go.png", - "Go/skills/20240818-21-08-37/Go.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859607/Go/skills/20240818-21-08-37/Go.png", - "Go/skills/20240830-19-46-09/Go.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859608/Go/skills/20240830-19-46-09/Go.png", - "GraphQL/skills/20240803-20-04-17/GraphQL": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859609/GraphQL/skills/20240803-20-04-17/GraphQL.png", - "GraphQL/skills/20240803-20-07-05/GraphQL": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859609/GraphQL/skills/20240803-20-07-05/GraphQL.png", - "GraphQL/skills/20240803-20-48-06/GraphQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859610/GraphQL/skills/20240803-20-48-06/GraphQL.png", - "GraphQL/skills/20240803-21-22-10/GraphQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859611/GraphQL/skills/20240803-21-22-10/GraphQL.png", - "GraphQL/skills/20240803-21-28-03/GraphQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859612/GraphQL/skills/20240803-21-28-03/GraphQL.png", - "GraphQL/skills/20240805-15-00-17/GraphQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859613/GraphQL/skills/20240805-15-00-17/GraphQL.png", - "GraphQL/skills/20240818-21-08-39/GraphQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859616/GraphQL/skills/20240818-21-08-39/GraphQL.png", - "GraphQL/skills/20240830-19-46-15/GraphQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859616/GraphQL/skills/20240830-19-46-15/GraphQL.png", - "Heroku/skills/20240803-20-04-19/Heroku": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859617/Heroku/skills/20240803-20-04-19/Heroku.png", - "Heroku/skills/20240803-20-07-06/Heroku": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859618/Heroku/skills/20240803-20-07-06/Heroku.png", - "Heroku/skills/20240803-20-48-08/Heroku.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859619/Heroku/skills/20240803-20-48-08/Heroku.png", - "Heroku/skills/20240803-21-22-10/Heroku.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859620/Heroku/skills/20240803-21-22-10/Heroku.png", - "Heroku/skills/20240803-21-28-04/Heroku.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859620/Heroku/skills/20240803-21-28-04/Heroku.png", - "Heroku/skills/20240805-15-00-19/Heroku.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859621/Heroku/skills/20240805-15-00-19/Heroku.png", - "Heroku/skills/20240818-21-08-42/Heroku.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859622/Heroku/skills/20240818-21-08-42/Heroku.png", - "Heroku/skills/20240830-19-46-22/Heroku.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859622/Heroku/skills/20240830-19-46-22/Heroku.png", - "Hibernate/skills/20240803-20-04-20/Hibernate": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859623/Hibernate/skills/20240803-20-04-20/Hibernate.png", - "Hibernate/skills/20240803-20-07-07/Hibernate": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859624/Hibernate/skills/20240803-20-07-07/Hibernate.png", - "Hibernate/skills/20240803-20-48-09/Hibernate.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859624/Hibernate/skills/20240803-20-48-09/Hibernate.png", - "Hibernate/skills/20240803-21-22-10/Hibernate.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859625/Hibernate/skills/20240803-21-22-10/Hibernate.png", - "Hibernate/skills/20240803-21-28-06/Hibernate.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859626/Hibernate/skills/20240803-21-28-06/Hibernate.png", - "Hibernate/skills/20240805-15-00-20/Hibernate.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859627/Hibernate/skills/20240805-15-00-20/Hibernate.png", - "Hibernate/skills/20240818-21-08-43/Hibernate.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859627/Hibernate/skills/20240818-21-08-43/Hibernate.png", - "Hibernate/skills/20240830-19-46-28/Hibernate.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859628/Hibernate/skills/20240830-19-46-28/Hibernate.png", - "Java/skills/20240803-20-04-25/Java": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859628/Java/skills/20240803-20-04-25/Java.png", - "Java/skills/20240803-20-07-09/Java": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859629/Java/skills/20240803-20-07-09/Java.png", - "Java/skills/20240803-20-48-12/Java.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859629/Java/skills/20240803-20-48-12/Java.png", - "Java/skills/20240803-21-22-11/Java.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859630/Java/skills/20240803-21-22-11/Java.png", - "Java/skills/20240803-21-28-09/Java.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859631/Java/skills/20240803-21-28-09/Java.png", - "Java/skills/20240805-15-00-24/Java.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859633/Java/skills/20240805-15-00-24/Java.png", - "Java/skills/20240818-21-08-48/Java.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859633/Java/skills/20240818-21-08-48/Java.png", - "Java/skills/20240830-19-46-43/Java.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859634/Java/skills/20240830-19-46-43/Java.png", - "JavaScript/skills/20240803-20-04-26/JavaScript": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859635/JavaScript/skills/20240803-20-04-26/JavaScript.png", - "JavaScript/skills/20240803-20-07-10/JavaScript": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859636/JavaScript/skills/20240803-20-07-10/JavaScript.png", - "JavaScript/skills/20240803-20-48-13/JavaScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859637/JavaScript/skills/20240803-20-48-13/JavaScript.png", - "JavaScript/skills/20240803-21-22-11/JavaScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859637/JavaScript/skills/20240803-21-22-11/JavaScript.png", - "JavaScript/skills/20240803-21-28-10/JavaScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859638/JavaScript/skills/20240803-21-28-10/JavaScript.png", - "JavaScript/skills/20240805-15-00-25/JavaScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859643/JavaScript/skills/20240805-15-00-25/JavaScript.png", - "JavaScript/skills/20240818-21-08-50/JavaScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859644/JavaScript/skills/20240818-21-08-50/JavaScript.png", - "JavaScript/skills/20240830-19-46-48/JavaScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859645/JavaScript/skills/20240830-19-46-48/JavaScript.png", - "Jenkins/skills/20240803-20-04-28/Jenkins": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859646/Jenkins/skills/20240803-20-04-28/Jenkins.png", - "Jenkins/skills/20240803-20-07-11/Jenkins": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859646/Jenkins/skills/20240803-20-07-11/Jenkins.png", - "Jenkins/skills/20240803-20-48-14/Jenkins.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859648/Jenkins/skills/20240803-20-48-14/Jenkins.png", - "Jenkins/skills/20240803-21-22-12/Jenkins.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859648/Jenkins/skills/20240803-21-22-12/Jenkins.png", - "Jenkins/skills/20240803-21-28-11/Jenkins.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859649/Jenkins/skills/20240803-21-28-11/Jenkins.png", - "Jenkins/skills/20240805-15-00-27/Jenkins.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859650/Jenkins/skills/20240805-15-00-27/Jenkins.png", - "Jenkins/skills/20240818-21-08-51/Jenkins.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859651/Jenkins/skills/20240818-21-08-51/Jenkins.png", - "Jenkins/skills/20240830-19-46-55/Jenkins.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859651/Jenkins/skills/20240830-19-46-55/Jenkins.png", - "Kotlin/skills/20240803-20-04-32/Kotlin": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859652/Kotlin/skills/20240803-20-04-32/Kotlin.png", - "Kotlin/skills/20240803-20-07-14/Kotlin": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859653/Kotlin/skills/20240803-20-07-14/Kotlin.png", - "Kotlin/skills/20240803-20-48-16/Kotlin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859653/Kotlin/skills/20240803-20-48-16/Kotlin.png", - "Kotlin/skills/20240803-21-22-12/Kotlin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859654/Kotlin/skills/20240803-21-22-12/Kotlin.png", - "Kotlin/skills/20240803-21-28-16/Kotlin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859655/Kotlin/skills/20240803-21-28-16/Kotlin.png", - "Kotlin/skills/20240805-15-00-30/Kotlin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859656/Kotlin/skills/20240805-15-00-30/Kotlin.png", - "Kotlin/skills/20240818-21-09-01/Kotlin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859656/Kotlin/skills/20240818-21-09-01/Kotlin.png", - "Kotlin/skills/20240830-19-47-06/Kotlin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859657/Kotlin/skills/20240830-19-47-06/Kotlin.png", - "Kubernetes/skills/20240803-20-07-15/Kubernetes": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859658/Kubernetes/skills/20240803-20-07-15/Kubernetes.png", - "Kubernetes/skills/20240803-20-48-18/Kubernetes.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859658/Kubernetes/skills/20240803-20-48-18/Kubernetes.png", - "Kubernetes/skills/20240803-21-22-13/Kubernetes.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859659/Kubernetes/skills/20240803-21-22-13/Kubernetes.png", - "Kubernetes/skills/20240803-21-28-17/Kubernetes.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859660/Kubernetes/skills/20240803-21-28-17/Kubernetes.png", - "Kubernetes/skills/20240805-15-00-31/Kubernetes.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859661/Kubernetes/skills/20240805-15-00-31/Kubernetes.png", - "Kubernetes/skills/20240818-21-09-02/Kubernetes.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859661/Kubernetes/skills/20240818-21-09-02/Kubernetes.png", - "Kubernetes/skills/20240830-19-47-12/Kubernetes.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859662/Kubernetes/skills/20240830-19-47-12/Kubernetes.png", - "Laravel/skills/20240803-20-07-16/Laravel": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859662/Laravel/skills/20240803-20-07-16/Laravel.png", - "Laravel/skills/20240803-20-48-19/Laravel.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859663/Laravel/skills/20240803-20-48-19/Laravel.png", - "Laravel/skills/20240803-21-22-13/Laravel.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859664/Laravel/skills/20240803-21-22-13/Laravel.png", - "Laravel/skills/20240803-21-28-25/Laravel.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859665/Laravel/skills/20240803-21-28-25/Laravel.png", - "Laravel/skills/20240805-15-00-32/Laravel.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859665/Laravel/skills/20240805-15-00-32/Laravel.png", - "Laravel/skills/20240818-21-09-04/Laravel.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859666/Laravel/skills/20240818-21-09-04/Laravel.png", - "Laravel/skills/20240830-19-47-19/Laravel.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859667/Laravel/skills/20240830-19-47-19/Laravel.png", - "Lua/skills/20240803-20-07-17/Lua": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859668/Lua/skills/20240803-20-07-17/Lua.png", - "Lua/skills/20240803-20-48-20/Lua.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859668/Lua/skills/20240803-20-48-20/Lua.png", - "Lua/skills/20240803-21-22-13/Lua.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859669/Lua/skills/20240803-21-22-13/Lua.png", - "Lua/skills/20240803-21-28-26/Lua.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859670/Lua/skills/20240803-21-28-26/Lua.png", - "Lua/skills/20240805-15-00-34/Lua.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859671/Lua/skills/20240805-15-00-34/Lua.png", - "Lua/skills/20240818-21-09-11/Lua.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859672/Lua/skills/20240818-21-09-11/Lua.png", - "Lua/skills/20240830-19-47-26/Lua.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859672/Lua/skills/20240830-19-47-26/Lua.png", - "MongoDB/skills/20240803-20-07-19/MongoDB": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859673/MongoDB/skills/20240803-20-07-19/MongoDB.png", - "MongoDB/skills/20240803-20-48-22/MongoDB.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859674/MongoDB/skills/20240803-20-48-22/MongoDB.png", - "MongoDB/skills/20240803-21-22-14/MongoDB.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859676/MongoDB/skills/20240803-21-22-14/MongoDB.png", - "MongoDB/skills/20240803-21-28-28/MongoDB.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859677/MongoDB/skills/20240803-21-28-28/MongoDB.png", - "MongoDB/skills/20240805-15-00-37/MongoDB.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859678/MongoDB/skills/20240805-15-00-37/MongoDB.png", - "MongoDB/skills/20240818-21-09-17/MongoDB.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859679/MongoDB/skills/20240818-21-09-17/MongoDB.png", - "MongoDB/skills/20240830-19-47-36/MongoDB.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859679/MongoDB/skills/20240830-19-47-36/MongoDB.png", - "MySQL/skills/20240803-20-07-20/MySQL": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859680/MySQL/skills/20240803-20-07-20/MySQL.png", - "MySQL/skills/20240803-20-48-23/MySQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859681/MySQL/skills/20240803-20-48-23/MySQL.png", - "MySQL/skills/20240803-21-22-14/MySQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859682/MySQL/skills/20240803-21-22-14/MySQL.png", - "MySQL/skills/20240803-21-28-30/MySQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859682/MySQL/skills/20240803-21-28-30/MySQL.png", - "MySQL/skills/20240805-15-00-38/MySQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859683/MySQL/skills/20240805-15-00-38/MySQL.png", - "MySQL/skills/20240818-21-09-19/MySQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859684/MySQL/skills/20240818-21-09-19/MySQL.png", - "MySQL/skills/20240830-19-47-42/MySQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859685/MySQL/skills/20240830-19-47-42/MySQL.png", - "Neo4j/skills/20240803-20-07-21/Neo4j": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859686/Neo4j/skills/20240803-20-07-21/Neo4j.png", - "Neo4j/skills/20240803-20-48-25/Neo4j.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859686/Neo4j/skills/20240803-20-48-25/Neo4j.png", - "Neo4j/skills/20240803-21-22-15/Neo4j.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859687/Neo4j/skills/20240803-21-22-15/Neo4j.png", - "Neo4j/skills/20240803-21-28-32/Neo4j.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859688/Neo4j/skills/20240803-21-28-32/Neo4j.png", - "Neo4j/skills/20240805-15-00-40/Neo4j.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859689/Neo4j/skills/20240805-15-00-40/Neo4j.png", - "Neo4j/skills/20240818-21-09-21/Neo4j.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859689/Neo4j/skills/20240818-21-09-21/Neo4j.png", - "Neo4j/skills/20240830-19-47-48/Neo4j.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859691/Neo4j/skills/20240830-19-47-48/Neo4j.png", - "Nginx/skills/20240803-20-07-22/Nginx": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859692/Nginx/skills/20240803-20-07-22/Nginx.png", - "Nginx/skills/20240803-20-48-26/Nginx.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859693/Nginx/skills/20240803-20-48-26/Nginx.png", - "Nginx/skills/20240803-21-22-15/Nginx.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859694/Nginx/skills/20240803-21-22-15/Nginx.png", - "Nginx/skills/20240803-21-28-33/Nginx.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859695/Nginx/skills/20240803-21-28-33/Nginx.png", - "Nginx/skills/20240805-15-00-42/Nginx.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859696/Nginx/skills/20240805-15-00-42/Nginx.png", - "Nginx/skills/20240818-21-09-22/Nginx.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859696/Nginx/skills/20240818-21-09-22/Nginx.png", - "Nginx/skills/20240830-19-47-53/Nginx.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859697/Nginx/skills/20240830-19-47-53/Nginx.png", - "OpenCV/skills/20240803-20-07-24/OpenCV": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859697/OpenCV/skills/20240803-20-07-24/OpenCV.png", - "OpenCV/skills/20240803-20-48-27/OpenCV.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859698/OpenCV/skills/20240803-20-48-27/OpenCV.png", - "OpenCV/skills/20240803-21-22-15/OpenCV.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859699/OpenCV/skills/20240803-21-22-15/OpenCV.png", - "OpenCV/skills/20240803-21-28-35/OpenCV.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859700/OpenCV/skills/20240803-21-28-35/OpenCV.png", - "OpenCV/skills/20240805-15-00-43/OpenCV.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859701/OpenCV/skills/20240805-15-00-43/OpenCV.png", - "OpenCV/skills/20240818-21-09-25/OpenCV.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859701/OpenCV/skills/20240818-21-09-25/OpenCV.png", - "OpenCV/skills/20240830-19-48-01/OpenCV.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859702/OpenCV/skills/20240830-19-48-01/OpenCV.png", - "Oracle/skills/20240803-20-07-25/Oracle": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859703/Oracle/skills/20240803-20-07-25/Oracle.png", - "Oracle/skills/20240803-20-48-28/Oracle.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859704/Oracle/skills/20240803-20-48-28/Oracle.png", - "Oracle/skills/20240803-21-22-16/Oracle.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859705/Oracle/skills/20240803-21-22-16/Oracle.png", - "Oracle/skills/20240803-21-28-37/Oracle.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859706/Oracle/skills/20240803-21-28-37/Oracle.png", - "Oracle/skills/20240805-15-00-44/Oracle.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859706/Oracle/skills/20240805-15-00-44/Oracle.png", - "Oracle/skills/20240818-21-09-26/Oracle.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859707/Oracle/skills/20240818-21-09-26/Oracle.png", - "Oracle/skills/20240830-19-48-07/Oracle.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859708/Oracle/skills/20240830-19-48-07/Oracle.png", - "PHP/skills/20240803-20-07-27/PHP": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859709/PHP/skills/20240803-20-07-27/PHP.png", - "PHP/skills/20240803-20-48-31/PHP.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859710/PHP/skills/20240803-20-48-31/PHP.png", - "PHP/skills/20240803-21-22-16/PHP.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859711/PHP/skills/20240803-21-22-16/PHP.png", - "PHP/skills/20240803-21-28-40/PHP.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859711/PHP/skills/20240803-21-28-40/PHP.png", - "PHP/skills/20240805-15-00-49/PHP.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859712/PHP/skills/20240805-15-00-49/PHP.png", - "PHP/skills/20240818-21-09-30/PHP.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859712/PHP/skills/20240818-21-09-30/PHP.png", - "PHP/skills/20240830-19-48-22/PHP.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859714/PHP/skills/20240830-19-48-22/PHP.png", - "Perl/skills/20240803-20-07-26/Perl": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859714/Perl/skills/20240803-20-07-26/Perl.png", - "Perl/skills/20240803-20-48-30/Perl.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859715/Perl/skills/20240803-20-48-30/Perl.png", - "Perl/skills/20240803-21-22-16/Perl.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859716/Perl/skills/20240803-21-22-16/Perl.png", - "Perl/skills/20240803-21-28-38/Perl.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859717/Perl/skills/20240803-21-28-38/Perl.png", - "Perl/skills/20240805-15-00-47/Perl.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859717/Perl/skills/20240805-15-00-47/Perl.png", - "Perl/skills/20240818-21-09-28/Perl.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859718/Perl/skills/20240818-21-09-28/Perl.png", - "Perl/skills/20240830-19-48-14/Perl.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859719/Perl/skills/20240830-19-48-14/Perl.png", - "PostgreSQL/skills/20240803-20-07-28/PostgreSQL": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859720/PostgreSQL/skills/20240803-20-07-28/PostgreSQL.png", - "PostgreSQL/skills/20240803-20-48-32/PostgreSQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859721/PostgreSQL/skills/20240803-20-48-32/PostgreSQL.png", - "PostgreSQL/skills/20240803-21-22-17/PostgreSQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859721/PostgreSQL/skills/20240803-21-22-17/PostgreSQL.png", - "PostgreSQL/skills/20240803-21-28-42/PostgreSQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859722/PostgreSQL/skills/20240803-21-28-42/PostgreSQL.png", - "PostgreSQL/skills/20240805-15-00-50/PostgreSQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859723/PostgreSQL/skills/20240805-15-00-50/PostgreSQL.png", - "PostgreSQL/skills/20240818-21-09-31/PostgreSQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859724/PostgreSQL/skills/20240818-21-09-31/PostgreSQL.png", - "PostgreSQL/skills/20240830-19-48-28/PostgreSQL.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859724/PostgreSQL/skills/20240830-19-48-28/PostgreSQL.png", - "PyTorch/skills/20240803-20-07-29/PyTorch": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859725/PyTorch/skills/20240803-20-07-29/PyTorch.png", - "PyTorch/skills/20240803-20-48-34/PyTorch.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859726/PyTorch/skills/20240803-20-48-34/PyTorch.png", - "PyTorch/skills/20240803-21-22-17/PyTorch.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859727/PyTorch/skills/20240803-21-22-17/PyTorch.png", - "PyTorch/skills/20240803-21-28-44/PyTorch.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859727/PyTorch/skills/20240803-21-28-44/PyTorch.png", - "PyTorch/skills/20240805-15-00-52/PyTorch.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859728/PyTorch/skills/20240805-15-00-52/PyTorch.png", - "PyTorch/skills/20240818-21-09-33/PyTorch.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859729/PyTorch/skills/20240818-21-09-33/PyTorch.png", - "PyTorch/skills/20240830-19-48-35/PyTorch.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859730/PyTorch/skills/20240830-19-48-35/PyTorch.png", - "Python/skills/20240803-20-07-30/Python": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859731/Python/skills/20240803-20-07-30/Python.png", - "Python/skills/20240803-20-48-36/Python.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859732/Python/skills/20240803-20-48-36/Python.png", - "Python/skills/20240803-21-22-17/Python.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859733/Python/skills/20240803-21-22-17/Python.png", - "Python/skills/20240803-21-28-47/Python.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859733/Python/skills/20240803-21-28-47/Python.png", - "Python/skills/20240805-15-00-54/Python.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859735/Python/skills/20240805-15-00-54/Python.png", - "Python/skills/20240818-21-09-34/Python.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859736/Python/skills/20240818-21-09-34/Python.png", - "Python/skills/20240830-19-48-43/Python.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859737/Python/skills/20240830-19-48-43/Python.png", - "React/skills/20240803-20-07-31/React": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859737/React/skills/20240803-20-07-31/React.png", - "React/skills/20240803-20-48-37/React.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859738/React/skills/20240803-20-48-37/React.png", - "React/skills/20240803-21-22-18/React.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859739/React/skills/20240803-21-22-18/React.png", - "React/skills/20240803-21-28-48/React.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859739/React/skills/20240803-21-28-48/React.png", - "React/skills/20240805-15-00-55/React.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859740/React/skills/20240805-15-00-55/React.png", - "React/skills/20240818-21-09-36/React.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859741/React/skills/20240818-21-09-36/React.png", - "React/skills/20240830-19-48-49/React.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859742/React/skills/20240830-19-48-49/React.png", - "Redis/skills/20240803-20-07-33/Redis": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859745/Redis/skills/20240803-20-07-33/Redis.png", - "Redis/skills/20240803-20-48-38/Redis.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859746/Redis/skills/20240803-20-48-38/Redis.png", - "Redis/skills/20240803-21-22-18/Redis.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859746/Redis/skills/20240803-21-22-18/Redis.png", - "Redis/skills/20240803-21-28-50/Redis.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859747/Redis/skills/20240803-21-28-50/Redis.png", - "Redis/skills/20240805-15-00-57/Redis.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859748/Redis/skills/20240805-15-00-57/Redis.png", - "Redis/skills/20240818-21-09-38/Redis.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859748/Redis/skills/20240818-21-09-38/Redis.png", - "Redis/skills/20240830-19-48-56/Redis.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859749/Redis/skills/20240830-19-48-56/Redis.png", - "Redux/skills/20240803-20-07-34/Redux": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859750/Redux/skills/20240803-20-07-34/Redux.png", - "Redux/skills/20240803-20-48-39/Redux.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859751/Redux/skills/20240803-20-48-39/Redux.png", - "Redux/skills/20240803-21-22-18/Redux.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859752/Redux/skills/20240803-21-22-18/Redux.png", - "Redux/skills/20240803-21-28-51/Redux.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859752/Redux/skills/20240803-21-28-51/Redux.png", - "Redux/skills/20240805-15-00-58/Redux.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859753/Redux/skills/20240805-15-00-58/Redux.png", - "Redux/skills/20240818-21-09-39/Redux.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859754/Redux/skills/20240818-21-09-39/Redux.png", - "Redux/skills/20240830-19-49-04/Redux.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859754/Redux/skills/20240830-19-49-04/Redux.png", - "Redux-Saga/skills/20240803-20-07-35/ReduxSaga": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859755/Redux-Saga/skills/20240803-20-07-35/ReduxSaga.png", - "Redux-Saga/skills/20240803-20-48-40/ReduxSaga.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859755/Redux-Saga/skills/20240803-20-48-40/ReduxSaga.png", - "Redux-Saga/skills/20240803-21-22-19/ReduxSaga.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859757/Redux-Saga/skills/20240803-21-22-19/ReduxSaga.png", - "Redux-Saga/skills/20240803-21-28-52/ReduxSaga.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859759/Redux-Saga/skills/20240803-21-28-52/ReduxSaga.png", - "Redux-Saga/skills/20240805-15-00-59/ReduxSaga.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859760/Redux-Saga/skills/20240805-15-00-59/ReduxSaga.png", - "Redux-Saga/skills/20240818-21-09-42/ReduxSaga.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859761/Redux-Saga/skills/20240818-21-09-42/ReduxSaga.png", - "Redux-Saga/skills/20240830-19-49-11/ReduxSaga.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859761/Redux-Saga/skills/20240830-19-49-11/ReduxSaga.png", - "Ruby/skills/20240803-20-07-36/Ruby": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859762/Ruby/skills/20240803-20-07-36/Ruby.png", - "Ruby/skills/20240803-20-48-42/Ruby.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859763/Ruby/skills/20240803-20-48-42/Ruby.png", - "Ruby/skills/20240803-21-22-19/Ruby.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859763/Ruby/skills/20240803-21-22-19/Ruby.png", - "Ruby/skills/20240803-21-28-54/Ruby.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859764/Ruby/skills/20240803-21-28-54/Ruby.png", - "Ruby/skills/20240805-15-01-01/Ruby.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859765/Ruby/skills/20240805-15-01-01/Ruby.png", - "Ruby/skills/20240818-21-09-44/Ruby.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859765/Ruby/skills/20240818-21-09-44/Ruby.png", - "Ruby/skills/20240830-19-49-18/Ruby.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859766/Ruby/skills/20240830-19-49-18/Ruby.png", - "Rust/skills/20240803-20-07-37/Rust": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859767/Rust/skills/20240803-20-07-37/Rust.png", - "Rust/skills/20240803-20-48-43/Rust.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859768/Rust/skills/20240803-20-48-43/Rust.png", - "Rust/skills/20240803-21-22-20/Rust.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859768/Rust/skills/20240803-21-22-20/Rust.png", - "Rust/skills/20240803-21-28-55/Rust.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859769/Rust/skills/20240803-21-28-55/Rust.png", - "Rust/skills/20240805-15-01-03/Rust.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859770/Rust/skills/20240805-15-01-03/Rust.png", - "Rust/skills/20240818-21-09-48/Rust.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859771/Rust/skills/20240818-21-09-48/Rust.png", - "Rust/skills/20240830-19-49-25/Rust.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859771/Rust/skills/20240830-19-49-25/Rust.png", - "SQLite/skills/20240803-20-07-41/SQLite": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859772/SQLite/skills/20240803-20-07-41/SQLite.png", - "SQLite/skills/20240803-20-48-46/SQLite.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859773/SQLite/skills/20240803-20-48-46/SQLite.png", - "SQLite/skills/20240803-21-22-21/SQLite.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859773/SQLite/skills/20240803-21-22-21/SQLite.png", - "SQLite/skills/20240803-21-28-59/SQLite.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859774/SQLite/skills/20240803-21-28-59/SQLite.png", - "SQLite/skills/20240805-15-01-08/SQLite.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859775/SQLite/skills/20240805-15-01-08/SQLite.png", - "SQLite/skills/20240818-21-09-53/SQLite.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859776/SQLite/skills/20240818-21-09-53/SQLite.png", - "SQLite/skills/20240830-19-49-43/SQLite.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859777/SQLite/skills/20240830-19-49-43/SQLite.png", - "Scala/skills/20240803-20-07-39/Scala": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859777/Scala/skills/20240803-20-07-39/Scala.png", - "Scala/skills/20240803-20-48-44/Scala.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859778/Scala/skills/20240803-20-48-44/Scala.png", - "Scala/skills/20240803-21-22-21/Scala.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859779/Scala/skills/20240803-21-22-21/Scala.png", - "Scala/skills/20240803-21-28-56/Scala.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859779/Scala/skills/20240803-21-28-56/Scala.png", - "Scala/skills/20240805-15-01-04/Scala.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859780/Scala/skills/20240805-15-01-04/Scala.png", - "Scala/skills/20240818-21-09-50/Scala.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859781/Scala/skills/20240818-21-09-50/Scala.png", - "Scala/skills/20240830-19-49-33/Scala.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859782/Scala/skills/20240830-19-49-33/Scala.png", - "SundayTechNuggets.jpeg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859785/SundayTechNuggets.jpg", - "Swift/skills/20240803-20-07-42/Swift": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859786/Swift/skills/20240803-20-07-42/Swift.png", - "Swift/skills/20240803-20-48-47/Swift.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859786/Swift/skills/20240803-20-48-47/Swift.png", - "Swift/skills/20240803-21-22-22/Swift.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859787/Swift/skills/20240803-21-22-22/Swift.png", - "Swift/skills/20240803-21-29-00/Swift.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859788/Swift/skills/20240803-21-29-00/Swift.png", - "Swift/skills/20240805-15-01-10/Swift.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859788/Swift/skills/20240805-15-01-10/Swift.png", - "Swift/skills/20240818-21-09-55/Swift.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859789/Swift/skills/20240818-21-09-55/Swift.png", - "Swift/skills/20240830-19-49-50/Swift.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859790/Swift/skills/20240830-19-49-50/Swift.png", - "TensorFlow/skills/20240803-20-07-43/TensorFlow": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859790/TensorFlow/skills/20240803-20-07-43/TensorFlow.png", - "TensorFlow/skills/20240803-20-48-48/TensorFlow.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859791/TensorFlow/skills/20240803-20-48-48/TensorFlow.png", - "TensorFlow/skills/20240803-21-22-22/TensorFlow.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859792/TensorFlow/skills/20240803-21-22-22/TensorFlow.png", - "TensorFlow/skills/20240803-21-29-01/TensorFlow.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859792/TensorFlow/skills/20240803-21-29-01/TensorFlow.png", - "TensorFlow/skills/20240805-15-01-11/TensorFlow.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859793/TensorFlow/skills/20240805-15-01-11/TensorFlow.png", - "TensorFlow/skills/20240818-21-09-57/TensorFlow.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859794/TensorFlow/skills/20240818-21-09-57/TensorFlow.png", - "TensorFlow/skills/20240830-19-49-56/TensorFlow.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859794/TensorFlow/skills/20240830-19-49-56/TensorFlow.png", - "Terraform/skills/20240803-20-07-44/Terraform": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859795/Terraform/skills/20240803-20-07-44/Terraform.png", - "Terraform/skills/20240803-20-48-49/Terraform.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859796/Terraform/skills/20240803-20-48-49/Terraform.png", - "Terraform/skills/20240803-21-22-22/Terraform.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859797/Terraform/skills/20240803-21-22-22/Terraform.png", - "Terraform/skills/20240803-21-29-02/Terraform.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859798/Terraform/skills/20240803-21-29-02/Terraform.png", - "Terraform/skills/20240805-15-01-12/Terraform.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859799/Terraform/skills/20240805-15-01-12/Terraform.png", - "Terraform/skills/20240818-21-09-58/Terraform.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859800/Terraform/skills/20240818-21-09-58/Terraform.png", - "Terraform/skills/20240830-19-50-02/Terraform.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859800/Terraform/skills/20240830-19-50-02/Terraform.png", - "TypeScript/skills/20240803-20-07-45/TypeScript": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859801/TypeScript/skills/20240803-20-07-45/TypeScript.png", - "TypeScript/skills/20240803-20-48-51/TypeScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859802/TypeScript/skills/20240803-20-48-51/TypeScript.png", - "TypeScript/skills/20240803-21-22-23/TypeScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859802/TypeScript/skills/20240803-21-22-23/TypeScript.png", - "TypeScript/skills/20240803-21-29-04/TypeScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859803/TypeScript/skills/20240803-21-29-04/TypeScript.png", - "TypeScript/skills/20240805-15-01-14/TypeScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859804/TypeScript/skills/20240805-15-01-14/TypeScript.png", - "TypeScript/skills/20240818-21-10-00/TypeScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859805/TypeScript/skills/20240818-21-10-00/TypeScript.png", - "TypeScript/skills/20240830-19-50-19/TypeScript.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859805/TypeScript/skills/20240830-19-50-19/TypeScript.png", - "Vagrant/skills/20240803-20-07-47/Vagrant": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859806/Vagrant/skills/20240803-20-07-47/Vagrant.png", - "Vagrant/skills/20240803-20-48-53/Vagrant.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859807/Vagrant/skills/20240803-20-48-53/Vagrant.png", - "Vagrant/skills/20240803-21-22-23/Vagrant.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859807/Vagrant/skills/20240803-21-22-23/Vagrant.png", - "Vagrant/skills/20240803-21-29-06/Vagrant.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859808/Vagrant/skills/20240803-21-29-06/Vagrant.png", - "Vagrant/skills/20240805-15-01-16/Vagrant.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859809/Vagrant/skills/20240805-15-01-16/Vagrant.png", - "Vagrant/skills/20240818-21-10-03/Vagrant.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859810/Vagrant/skills/20240818-21-10-03/Vagrant.png", - "Vagrant/skills/20240830-19-50-30/Vagrant.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859811/Vagrant/skills/20240830-19-50-30/Vagrant.png", - "Xamarin/skills/20240803-20-07-49/Xamarin": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859812/Xamarin/skills/20240803-20-07-49/Xamarin.png", - "Xamarin/skills/20240803-20-48-57/Xamarin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859813/Xamarin/skills/20240803-20-48-57/Xamarin.png", - "Xamarin/skills/20240803-21-22-24/Xamarin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859814/Xamarin/skills/20240803-21-22-24/Xamarin.png", - "Xamarin/skills/20240803-21-29-08/Xamarin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859814/Xamarin/skills/20240803-21-29-08/Xamarin.png", - "Xamarin/skills/20240805-15-01-19/Xamarin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859815/Xamarin/skills/20240805-15-01-19/Xamarin.png", - "Xamarin/skills/20240818-21-10-07/Xamarin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859816/Xamarin/skills/20240818-21-10-07/Xamarin.png", - "Xamarin/skills/20240830-19-50-40/Xamarin.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859816/Xamarin/skills/20240830-19-50-40/Xamarin.png", - "benedictvimdey9/feed/20231215-10-32-11/WhatsAppImage2023-12-14at8.18.54AM.jpeg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859818/benedictvimdey9/feed/20231215-10-32-11/WhatsAppImage2023-12-14at8.18.54AM.jpg", - "benedictvimdey9/profile/20231215-10-29-39/WhatsAppImage2023-12-14at8.18.54AM.jpeg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859819/benedictvimdey9/profile/20231215-10-29-39/WhatsAppImage2023-12-14at8.18.54AM.jpg", - "briannewton5/feed/20231204-20-42-46/obamaawardingobama.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859822/briannewton5/feed/20231204-20-42-46/obamaawardingobama.png", - "briannewton5/feed/20231204-21-26-43/obamaawardingobama.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859823/briannewton5/feed/20231204-21-26-43/obamaawardingobama.png", - "briannewton5/profile/20231204-20-49-16/obamaawardingobama.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859824/briannewton5/profile/20231204-20-49-16/obamaawardingobama.png", - "briannewton5/profile/20231204-21-29-45/IMG_6523.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859825/briannewton5/profile/20231204-21-29-45/IMG_6523.jpg", - "iOS/skills/20240803-20-04-23/iOS": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859826/iOS/skills/20240803-20-04-23/iOS.png", - "iOS/skills/20240803-20-07-08/iOS": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859827/iOS/skills/20240803-20-07-08/iOS.png", - "iOS/skills/20240803-20-48-11/iOS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859828/iOS/skills/20240803-20-48-11/iOS.png", - "iOS/skills/20240803-21-22-11/iOS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859829/iOS/skills/20240803-21-22-11/iOS.png", - "iOS/skills/20240803-21-28-07/iOS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859829/iOS/skills/20240803-21-28-07/iOS.png", - "iOS/skills/20240805-15-00-23/iOS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859830/iOS/skills/20240805-15-00-23/iOS.png", - "iOS/skills/20240818-21-08-46/iOS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859830/iOS/skills/20240818-21-08-46/iOS.png", - "iOS/skills/20240830-19-46-36/iOS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859831/iOS/skills/20240830-19-46-36/iOS.png", - "jQuery/skills/20240803-20-04-29/jQuery": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859832/jQuery/skills/20240803-20-04-29/jQuery.png", - "jQuery/skills/20240803-20-07-12/jQuery": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859832/jQuery/skills/20240803-20-07-12/jQuery.png", - "jQuery/skills/20240803-20-48-15/jQuery.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859833/jQuery/skills/20240803-20-48-15/jQuery.png", - "jQuery/skills/20240803-21-22-12/jQuery.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859834/jQuery/skills/20240803-21-22-12/jQuery.png", - "jQuery/skills/20240803-21-28-13/jQuery.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859834/jQuery/skills/20240803-21-28-13/jQuery.png", - "jQuery/skills/20240805-15-00-28/jQuery.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859835/jQuery/skills/20240805-15-00-28/jQuery.png", - "jQuery/skills/20240818-21-08-53/jQuery.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859836/jQuery/skills/20240818-21-08-53/jQuery.png", - "jQuery/skills/20240830-19-47-00/jQuery.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859836/jQuery/skills/20240830-19-47-00/jQuery.png", - "johnsonquame20/profile/20241112-13-30-56/my_avatar.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859838/johnsonquame20/profile/20241112-13-30-56/my_avatar.png", - "jonathanmarkin8/feed/20240209-06-49-27/1000150577.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859839/jonathanmarkin8/feed/20240209-06-49-27/1000150577.jpg", - "jonathanmarkin8/profile/20250115-11-54-17/pofile.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859842/jonathanmarkin8/profile/20250115-11-54-17/pofile.jpg", - "kwesidadson1/profile/20231211-22-08-09/WhatsAppImage2023-08-22at6.23.10PM.jpeg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859843/kwesidadson1/profile/20231211-22-08-09/WhatsAppImage2023-08-22at6.23.10PM.jpg", - "marvinkudjo45/profile/20231207-21-44-49/thug.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859844/marvinkudjo45/profile/20231207-21-44-49/thug.jpg", - "neilohene161/profile/20240120-12-27-45/me.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859845/neilohene161/profile/20240120-12-27-45/me.png", - "philipabakah43/profile/20231218-23-03-37/photo1697650501.jpeg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859846/philipabakah43/profile/20231218-23-03-37/photo1697650501.jpg", - "ransfordgenesis/feed/20231204-21-17-04/meme.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859847/ransfordgenesis/feed/20231204-21-17-04/meme.jpg", - "ransfordgenesis/profile/20231207-16-51-17/bighead.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859847/ransfordgenesis/profile/20231207-16-51-17/bighead.jpg", - "ransfordgenesis/profile/20231207-16-51-21/bighead.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859848/ransfordgenesis/profile/20231207-16-51-21/bighead.jpg", - "stncrmutilities/AWS/skills/20240803-20-03-57/AWS": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859849/stncrmutilities/AWS/skills/20240803-20-03-57/AWS.png", - "stncrmutilities/AWS/skills/20240803-20-06-47/AWS": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859850/stncrmutilities/AWS/skills/20240803-20-06-47/AWS.png", - "stncrmutilities/AWS/skills/20240803-20-45-22/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859852/stncrmutilities/AWS/skills/20240803-20-45-22/AWS.png", - "stncrmutilities/AWS/skills/20240803-20-47-47/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859852/stncrmutilities/AWS/skills/20240803-20-47-47/AWS.png", - "stncrmutilities/AWS/skills/20240803-21-22-04/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859853/stncrmutilities/AWS/skills/20240803-21-22-04/AWS.png", - "stncrmutilities/AWS/skills/20240803-21-27-40/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859853/stncrmutilities/AWS/skills/20240803-21-27-40/AWS.png", - "stncrmutilities/AWS/skills/20240805-14-59-51/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859854/stncrmutilities/AWS/skills/20240805-14-59-51/AWS.png", - "stncrmutilities/AWS/skills/20240818-21-07-45/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859855/stncrmutilities/AWS/skills/20240818-21-07-45/AWS.png", - "stncrmutilities/AWS/skills/20240830-19-44-41/AWS.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859855/stncrmutilities/AWS/skills/20240830-19-44-41/AWS.png", - "stncrmutilities/Android/skills/20240803-19-48-30/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859856/stncrmutilities/Android/skills/20240803-19-48-30/Android.png", - "stncrmutilities/Android/skills/20240803-19-50-17/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859857/stncrmutilities/Android/skills/20240803-19-50-17/Android.png", - "stncrmutilities/Android/skills/20240803-19-51-07/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859858/stncrmutilities/Android/skills/20240803-19-51-07/Android.png", - "stncrmutilities/Android/skills/20240803-19-51-47/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859859/stncrmutilities/Android/skills/20240803-19-51-47/Android.png", - "stncrmutilities/Android/skills/20240803-19-53-00/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859859/stncrmutilities/Android/skills/20240803-19-53-00/Android.png", - "stncrmutilities/Android/skills/20240803-19-53-27/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859860/stncrmutilities/Android/skills/20240803-19-53-27/Android.png", - "stncrmutilities/Android/skills/20240803-19-54-26/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859861/stncrmutilities/Android/skills/20240803-19-54-26/Android.png", - "stncrmutilities/Android/skills/20240803-19-56-10/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859861/stncrmutilities/Android/skills/20240803-19-56-10/Android.png", - "stncrmutilities/Android/skills/20240803-19-56-32/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859862/stncrmutilities/Android/skills/20240803-19-56-32/Android.png", - "stncrmutilities/Android/skills/20240803-19-57-51/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859863/stncrmutilities/Android/skills/20240803-19-57-51/Android.png", - "stncrmutilities/Android/skills/20240803-19-58-51/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859863/stncrmutilities/Android/skills/20240803-19-58-51/Android.png", - "stncrmutilities/Android/skills/20240803-20-03-16/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859864/stncrmutilities/Android/skills/20240803-20-03-16/Android.png", - "stncrmutilities/Android/skills/20240803-20-03-52/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859865/stncrmutilities/Android/skills/20240803-20-03-52/Android.png", - "stncrmutilities/Android/skills/20240803-20-06-42/Android": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859866/stncrmutilities/Android/skills/20240803-20-06-42/Android.png", - "stncrmutilities/Android/skills/20240803-20-45-16/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859867/stncrmutilities/Android/skills/20240803-20-45-16/Android.png", - "stncrmutilities/Android/skills/20240803-20-47-42/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859867/stncrmutilities/Android/skills/20240803-20-47-42/Android.png", - "stncrmutilities/Android/skills/20240803-21-22-03/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859868/stncrmutilities/Android/skills/20240803-21-22-03/Android.png", - "stncrmutilities/Android/skills/20240803-21-27-33/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859869/stncrmutilities/Android/skills/20240803-21-27-33/Android.png", - "stncrmutilities/Android/skills/20240805-14-59-44/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859869/stncrmutilities/Android/skills/20240805-14-59-44/Android.png", - "stncrmutilities/Android/skills/20240818-21-07-35/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859870/stncrmutilities/Android/skills/20240818-21-07-35/Android.png", - "stncrmutilities/Android/skills/20240830-19-44-12/Android.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859871/stncrmutilities/Android/skills/20240830-19-44-12/Android.png", - "stncrmutilities/Angular/skills/20240803-19-48-31/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859872/stncrmutilities/Angular/skills/20240803-19-48-31/Angular.png", - "stncrmutilities/Angular/skills/20240803-19-50-19/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859873/stncrmutilities/Angular/skills/20240803-19-50-19/Angular.png", - "stncrmutilities/Angular/skills/20240803-19-51-08/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859873/stncrmutilities/Angular/skills/20240803-19-51-08/Angular.png", - "stncrmutilities/Angular/skills/20240803-19-51-49/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859874/stncrmutilities/Angular/skills/20240803-19-51-49/Angular.png", - "stncrmutilities/Angular/skills/20240803-19-53-01/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859875/stncrmutilities/Angular/skills/20240803-19-53-01/Angular.png", - "stncrmutilities/Angular/skills/20240803-19-53-28/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859876/stncrmutilities/Angular/skills/20240803-19-53-28/Angular.png", - "stncrmutilities/Angular/skills/20240803-19-54-27/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859877/stncrmutilities/Angular/skills/20240803-19-54-27/Angular.png", - "stncrmutilities/Angular/skills/20240803-19-56-11/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859878/stncrmutilities/Angular/skills/20240803-19-56-11/Angular.png", - "stncrmutilities/Angular/skills/20240803-19-56-33/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859879/stncrmutilities/Angular/skills/20240803-19-56-33/Angular.png", - "stncrmutilities/Angular/skills/20240803-19-57-52/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859879/stncrmutilities/Angular/skills/20240803-19-57-52/Angular.png", - "stncrmutilities/Angular/skills/20240803-19-58-53/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859880/stncrmutilities/Angular/skills/20240803-19-58-53/Angular.png", - "stncrmutilities/Angular/skills/20240803-20-03-17/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859881/stncrmutilities/Angular/skills/20240803-20-03-17/Angular.png", - "stncrmutilities/Angular/skills/20240803-20-03-53/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859881/stncrmutilities/Angular/skills/20240803-20-03-53/Angular.png", - "stncrmutilities/Angular/skills/20240803-20-06-43/Angular": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859882/stncrmutilities/Angular/skills/20240803-20-06-43/Angular.png", - "stncrmutilities/Angular/skills/20240803-20-45-18/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859883/stncrmutilities/Angular/skills/20240803-20-45-18/Angular.png", - "stncrmutilities/Angular/skills/20240803-20-47-43/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859883/stncrmutilities/Angular/skills/20240803-20-47-43/Angular.png", - "stncrmutilities/Angular/skills/20240803-21-22-03/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859884/stncrmutilities/Angular/skills/20240803-21-22-03/Angular.png", - "stncrmutilities/Angular/skills/20240803-21-27-34/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859885/stncrmutilities/Angular/skills/20240803-21-27-34/Angular.png", - "stncrmutilities/Angular/skills/20240805-14-59-46/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859886/stncrmutilities/Angular/skills/20240805-14-59-46/Angular.png", - "stncrmutilities/Angular/skills/20240818-21-07-38/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859887/stncrmutilities/Angular/skills/20240818-21-07-38/Angular.png", - "stncrmutilities/Angular/skills/20240830-19-44-19/Angular.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859887/stncrmutilities/Angular/skills/20240830-19-44-19/Angular.png", - "stncrmutilities/Ansible/skills/20240803-20-03-54/Ansible": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859888/stncrmutilities/Ansible/skills/20240803-20-03-54/Ansible.png", - "stncrmutilities/Ansible/skills/20240803-20-06-45/Ansible": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859889/stncrmutilities/Ansible/skills/20240803-20-06-45/Ansible.png", - "stncrmutilities/Ansible/skills/20240803-20-45-19/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859890/stncrmutilities/Ansible/skills/20240803-20-45-19/Ansible.png", - "stncrmutilities/Ansible/skills/20240803-20-47-44/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859890/stncrmutilities/Ansible/skills/20240803-20-47-44/Ansible.png", - "stncrmutilities/Ansible/skills/20240803-21-22-04/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859891/stncrmutilities/Ansible/skills/20240803-21-22-04/Ansible.png", - "stncrmutilities/Ansible/skills/20240803-21-27-36/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859892/stncrmutilities/Ansible/skills/20240803-21-27-36/Ansible.png", - "stncrmutilities/Ansible/skills/20240805-14-59-48/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859892/stncrmutilities/Ansible/skills/20240805-14-59-48/Ansible.png", - "stncrmutilities/Ansible/skills/20240818-21-07-41/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859893/stncrmutilities/Ansible/skills/20240818-21-07-41/Ansible.png", - "stncrmutilities/Ansible/skills/20240830-19-44-27/Ansible.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859894/stncrmutilities/Ansible/skills/20240830-19-44-27/Ansible.png", - "tonnybrightsogli176/profile/20231204-21-11-12/bigquery_table.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859897/tonnybrightsogli176/profile/20231204-21-11-12/bigquery_table.png", - "topboyasante/feed/20231206-12-16-40/jb.webp": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859898/topboyasante/feed/20231206-12-16-40/jb.webp", - "topboyasante/feed/20231206-21-37-34/IMG_1413.jpeg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859900/topboyasante/feed/20231206-21-37-34/IMG_1413.jpg", - "topboyasante/profile/20231206-12-08-11/jb.webp": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859901/topboyasante/profile/20231206-12-08-11/jb.webp", - "tutorials/app/.env": "ERROR: Empty file", - "tutorials/app/stdocker/manage.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859902/tutorials/app/stdocker/manage.py", - "tutorials/app/stdocker/stdocker/__init__.py": "ERROR: Empty file", - "tutorials/app/stdocker/stdocker/asgi.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859903/tutorials/app/stdocker/stdocker/asgi.py", - "tutorials/app/stdocker/stdocker/settings.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859903/tutorials/app/stdocker/stdocker/settings.py", - "tutorials/app/stdocker/stdocker/urls.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859905/tutorials/app/stdocker/stdocker/urls.py", - "tutorials/app/stdocker/stdocker/wsgi.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859906/tutorials/app/stdocker/stdocker/wsgi.py", - "tutorials/hello/Dockerfile": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859906/tutorials/hello/Dockerfile", - "tutorials/hello/__pycache__/main.cpython-311.pyc": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859907/tutorials/hello/__pycache__/main.cpython-311.pyc", - "tutorials/hello/docker-compose.yml": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859907/tutorials/hello/docker-compose.yml", - "tutorials/hello/main.py": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859908/tutorials/hello/main.py", - "tutorials/hello/pyproject.toml": "https://res.cloudinary.com/db5tuaqog/raw/upload/v1774859909/tutorials/hello/pyproject.toml", - "williamtsikata23/feed/20231211-22-49-01/20231203_223901.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859910/williamtsikata23/feed/20231211-22-49-01/20231203_223901.jpg", - "yawaddodiabene10/feed/20231202-23-37-43/Screenshot2023-11-30232300.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859911/yawaddodiabene10/feed/20231202-23-37-43/Screenshot2023-11-30232300.png", - "yawaddodiabene10/feed/20231202-23-42-14/Screenshot2023-06-12211851.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859912/yawaddodiabene10/feed/20231202-23-42-14/Screenshot2023-06-12211851.png", - "yawaddodiabene10/feed/20240408-22-01-52/Screenshot2024-04-08202144.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859913/yawaddodiabene10/feed/20240408-22-01-52/Screenshot2024-04-08202144.png", - "yawaddodiabene10/profile/20231204-14-39-35/erwinsmith.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859914/yawaddodiabene10/profile/20231204-14-39-35/erwinsmith.jpg", - "yawaddodiabene10/profile/20231204-19-06-48/Minimalist-4k-Wallpaper-Hd-.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859916/yawaddodiabene10/profile/20231204-19-06-48/Minimalist-4k-Wallpaper-Hd-.jpg", - "yawaddodiabene10/profile/20231204-19-08-35/erwinsmith.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859917/yawaddodiabene10/profile/20231204-19-08-35/erwinsmith.jpg", - "yawaddodiabene10/profile/20231204-19-12-02/erwinsmith.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859918/yawaddodiabene10/profile/20231204-19-12-02/erwinsmith.jpg", - "yawaddodiabene10/profile/20231206-13-33-00/Screenshot2023-12-06112258.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859921/yawaddodiabene10/profile/20231206-13-33-00/Screenshot2023-12-06112258.png", - "yawaddodiabene10/profile/20231206-13-34-05/erwinsmith.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859922/yawaddodiabene10/profile/20231206-13-34-05/erwinsmith.jpg", - "yawaddodiabene10/profile/20231210-21-02-40/richard-horvath-RAZU_R66vUc-unsplash.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859923/yawaddodiabene10/profile/20231210-21-02-40/richard-horvath-RAZU_R66vUc-unsplash.jpg", - "yawaddodiabene10/profile/20231211-22-45-28/richard-horvath-_nWaeTF6qo0-unsplash.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859924/yawaddodiabene10/profile/20231211-22-45-28/richard-horvath-_nWaeTF6qo0-unsplash.jpg", - "yawaddodiabene10/profile/20231211-22-49-02/richard-horvath-RAZU_R66vUc-unsplash.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859925/yawaddodiabene10/profile/20231211-22-49-02/richard-horvath-RAZU_R66vUc-unsplash.jpg", - "yawaddodiabene10/profile/20240215-21-36-17/jakob-owens-n5wwck8ES4w-unsplash.jpg": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859927/yawaddodiabene10/profile/20240215-21-36-17/jakob-owens-n5wwck8ES4w-unsplash.jpg", - "yawaddodiabene10/profile/20240408-22-12-21/7e391e107968361.5fb38e8bb826d.png": "https://res.cloudinary.com/db5tuaqog/image/upload/v1774859928/yawaddodiabene10/profile/20240408-22-12-21/7e391e107968361.5fb38e8bb826d.png" -} \ No newline at end of file diff --git a/services/auth_service.py b/services/auth_service.py index 255db6f..3e11153 100644 --- a/services/auth_service.py +++ b/services/auth_service.py @@ -76,7 +76,8 @@ def login(self, email: str, password: str) -> Token: refresh_token=get_refresh_token(str(user.id)), token_type="Bearer", is_active=user.is_active, - user_status=user.status + user_status=user.status, + role=user.role ) def refresh(self, refresh_token: str) -> Token: diff --git a/services/org_chart_service.py b/services/org_chart_service.py index 57675ac..32c5622 100644 --- a/services/org_chart_service.py +++ b/services/org_chart_service.py @@ -76,22 +76,41 @@ def _recurse(uid: int) -> Optional[OrgChartNode]: # Read operations # ------------------------------------------------------------------ - def get_direct_subordinates(self, user_id: int) -> list[User]: + def get_direct_subordinates(self, user_id: int, filter_active: bool = False) -> list[User]: + """Get direct subordinates of a user. + + Args: + user_id: The manager's user ID + filter_active: If True, only return ACCEPTED + is_active users (default: False - show all assigned team members) + """ self._get_user_or_404(user_id) - return self.repo.get_direct_subordinates(user_id) + return self.repo.get_direct_subordinates(user_id, filter_active=filter_active) + + def get_subtree(self, root_id: int, max_depth: Optional[int] = None, filter_active: bool = True) -> OrgChartNode: + """Get the org chart subtree for a given root user. - def get_subtree(self, root_id: int, max_depth: Optional[int] = None) -> OrgChartNode: + Args: + root_id: The root user's ID + max_depth: Maximum depth to traverse + filter_active: If True, only include ACCEPTED + is_active users (default: True) + """ max_depth = max_depth if max_depth is not None else self.DEFAULT_MAX_DEPTH self._get_user_or_404(root_id) rows = self.repo.get_subtree_ids(root_id, max_depth) user_ids = [r["id"] for r in rows] users = self.repo.get_users_by_ids(user_ids) users_by_id = {u.id: u for u in users} - return self._build_tree(users_by_id, root_id, rows) + return self._build_tree_filtered(users_by_id, root_id, rows, filter_active) + + def get_full_org_chart(self, max_depth: Optional[int] = None, filter_active: bool = True) -> list[OrgChartNode]: + """Get the full organizational chart. - def get_full_org_chart(self, max_depth: Optional[int] = None) -> list[OrgChartNode]: + Args: + max_depth: Maximum depth to traverse for each root + filter_active: If True, only include ACCEPTED + is_active users (default: True) + """ max_depth = max_depth if max_depth is not None else self.DEFAULT_MAX_DEPTH - roots = self.repo.get_root_users() + roots = self.repo.get_root_users(filter_active=filter_active) if not roots: return [] result = [] @@ -100,9 +119,59 @@ def get_full_org_chart(self, max_depth: Optional[int] = None) -> list[OrgChartNo user_ids = [r["id"] for r in rows] users = self.repo.get_users_by_ids(user_ids) users_by_id = {u.id: u for u in users} - result.append(self._build_tree(users_by_id, root.id, rows)) + + # Only build tree if root user exists (it should since it came from get_root_users) + if root.id in users_by_id: + tree_node = self._build_tree_filtered(users_by_id, root.id, rows, filter_active) + if tree_node: + result.append(tree_node) return result + def _build_tree_filtered(self, users_by_id: dict[int, User], root_id: int, rows: list[dict], filter_active: bool) -> Optional[OrgChartNode]: + """Build tree with optional filtering of ACCEPTED + is_active users.""" + children_map: dict[int, list[int]] = {} + for row in rows: + pid = row["manager_id"] + if pid is not None and pid in users_by_id: + children_map.setdefault(pid, []).append(row["id"]) + + visited: set[int] = set() + + def _recurse(uid: int) -> Optional[OrgChartNode]: + if uid in visited or uid not in users_by_id: + return None + visited.add(uid) + u = users_by_id[uid] + + # Skip if filtering is enabled and user doesn't match + if filter_active and (u.status != "ACCEPTED" or not u.is_active): + return None + + child_ids = children_map.get(uid, []) + subs = [] + for cid in child_ids: + node = _recurse(cid) + if node is not None: + subs.append(node) + return OrgChartNode( + id=u.id, + first_name=u.first_name, + last_name=u.last_name, + username=u.username, + profile_pic_url=u.profile_pic_url, + role=u.role, + stack=u.stack, + manager_id=u.manager_id, + status=u.status, + is_active=u.is_active, + subordinates=subs, + ) + + node = _recurse(root_id) + if node is None: + return None + return node + def get_manager(self, user_id: int) -> Optional[User]: user = self._get_user_or_404(user_id) if user.manager_id is None: diff --git a/services/project_service.py b/services/project_service.py index 84e784a..aa4519f 100644 --- a/services/project_service.py +++ b/services/project_service.py @@ -5,6 +5,7 @@ from api.api_models.projects import CreateProject, UpdateProject from db.models.projects import Project from db.models.users import User +from db.models.users_projects import UserProject from db.repository.projects import ProjectRepository from db.repository.skills import SkillRepository from db.repository.stacks import StackRepository @@ -20,6 +21,18 @@ def __init__(self, project_repo: ProjectRepository, user_repo: UserRepository, self.stack_repo = stack_repo self.skill_repo = skill_repo + def _enrich_project_members_with_team(self, project: Project) -> Project: + """Add team role data from users_projects join table to project members.""" + for member in project.members: + user_project = self.project_repo.db.query(UserProject).filter( + UserProject.user_id == member.id, + UserProject.project_id == project.id + ).first() + if user_project: + # Dynamically set the team attribute for Pydantic serialization + object.__setattr__(member, 'team', user_project.team) + return project + def create_project(self, project_data: CreateProject) -> Project: manager = self.user_repo.get_by_id(project_data.manager_id) if not manager: @@ -28,17 +41,8 @@ def create_project(self, project_data: CreateProject) -> Project: data = project_data.model_dump(exclude=["members", "stacks", "project_tools"]) new_project = Project(**data) - if project_data.members: - seen = [] - for member_id in project_data.members: - member = self.user_repo.get_by_id(member_id) - if not member: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, - detail=f"User {member_id} not found") - if not member.is_active: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, - detail=f"{member.first_name} is not an active member") - new_project.members.append(member) + # Note: Members are NOT added here. They are added separately via the /add/{userId} endpoint + # with their team roles specified. This ensures the users_projects join table has the team field set. if project_data.stacks: seen = [] @@ -66,18 +70,21 @@ def create_project(self, project_data: CreateProject) -> Project: new_project.project_tools.append(skill) seen.append(skill_id) - return self.project_repo.save(new_project) + saved = self.project_repo.save(new_project) + return self._enrich_project_members_with_team(saved) def update_project(self, project_id: int, update_data: UpdateProject) -> Project: project = self.project_repo.get_by_id(project_id) if not project: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") - if update_data.manager_id and update_data.manager_id != project.manager_id: - if not self.user_repo.get_by_id(update_data.manager_id): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Manager not found") - return self.project_repo.update( - project, update_data.model_dump(exclude=["members", "stacks", "project_tools"]) + if update_data.manager_id is not None: + if update_data.manager_id != project.manager_id: + if not self.user_repo.get_by_id(update_data.manager_id): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Manager not found") + updated = self.project_repo.update( + project, update_data.model_dump(exclude=["members", "stacks", "project_tools"], exclude_none=True) ) + return self._enrich_project_members_with_team(updated) def delete_project(self, project_id: int) -> None: project = self.project_repo.get_by_id(project_id) @@ -89,7 +96,7 @@ def get_project(self, project_id: int) -> Project: project = self.project_repo.get_by_id(project_id) if not project: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") - return project + return self._enrich_project_members_with_team(project) def get_all_query(self): return self.project_repo.get_all_paginated_query() @@ -119,4 +126,13 @@ def remove_member(self, project_id: int, user_id: int) -> None: def get_project_members(self, project_id: int, team: Optional[ProjectTeam]) -> List[User]: if not self.project_repo.get_by_id(project_id): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") - return self.project_repo.get_members(project_id, team) + members = self.project_repo.get_members(project_id, team) + # Enrich members with team data + for member in members: + user_project = self.project_repo.db.query(UserProject).filter( + UserProject.user_id == member.id, + UserProject.project_id == project_id + ).first() + if user_project: + object.__setattr__(member, 'team', user_project.team) + return members diff --git a/services/skill_service.py b/services/skill_service.py index c79d646..b6ba10d 100644 --- a/services/skill_service.py +++ b/services/skill_service.py @@ -46,8 +46,17 @@ def populate_skills(self) -> dict: from utils.endpoints_status import create_signup_endpoint from utils.tools import tools as skills_data, get_skills_image - create_roles() - create_signup_endpoint(status=True) + # Try to create roles and signup endpoint, but don't fail if they already exist + # or if running in a test environment with different database session + try: + create_roles() + except Exception: + pass # Roles likely already exist + + try: + create_signup_endpoint(status=True) + except Exception: + pass # Endpoint likely already exists try: last_skill = None @@ -65,7 +74,12 @@ def search_skills(self, name: str) -> list[dict]: skills = self.skill_repo.get_all_flat() threshold = 78 return [ - {"skill_id": skill.id, "skill_name": skill.name} + { + "skill_id": skill.id, + "skill_name": skill.name, + + "image_url": skill.image_url or "" + } for skill in skills if fuzz.partial_ratio(name.lower(), skill.name.lower()) >= threshold ] diff --git a/services/technical_task_service.py b/services/technical_task_service.py index aa43ea5..64f0fd6 100644 --- a/services/technical_task_service.py +++ b/services/technical_task_service.py @@ -55,22 +55,53 @@ def delete_task(self, task_id: int) -> None: # --- Submissions --- def create_submission(self, current_user: User, data: dict) -> TechnicalTaskSubmission: - user_exp = get_key_by_value(current_user.years_of_experience) + # Check if user has required profile information + if not current_user.stack_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Please complete your profile by selecting a tech stack before submitting." + ) + + if current_user.years_of_experience is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Please complete your profile by adding years of experience before submitting." + ) + + try: + user_exp = get_key_by_value(current_user.years_of_experience) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) task = self.task_repo.get_by_stack_and_level(current_user.stack_id, user_exp) + if not task: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Cannot find a task for this submission" + detail=( + f"No technical task found for your stack and experience level " + f"(stack_id={current_user.stack_id}, experience_level={user_exp}). " + f"Please contact admin at info@slightlytechie.com." + ) ) - if self.submission_repo.get_existing_for_user(current_user.id): + + # Check if user already submitted + existing_submission = self.submission_repo.get_existing_for_user(current_user.id) + if existing_submission: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Multiple submissions not allowed. Kindly contact admin." + detail="You have already submitted a task. Multiple submissions are not allowed. Contact admin if you need to update your submission." ) + try: return self.submission_repo.create(task.id, current_user.id, data) except Exception as e: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Failed to submit task: {str(e)}" + ) def get_all_submissions(self) -> list[TechnicalTaskSubmission]: submissions = self.submission_repo.get_all() diff --git a/services/user_service.py b/services/user_service.py index 4ed8a70..2d792de 100644 --- a/services/user_service.py +++ b/services/user_service.py @@ -77,6 +77,10 @@ async def update_user_status(self, user_id: int, new_status: UserStatus) -> User detail=settings.ERRORS.get("INVALID ID")) self.user_repo.update_status(user, new_status) + # Activate user when status becomes ACCEPTED + if new_status == UserStatus.ACCEPTED and not user.is_active: + self.user_repo.activate(user) + if new_status in (UserStatus.ACCEPTED, UserStatus.REJECTED): try: template = self.email_template_repo.get_by_name(new_status.value) diff --git a/test/test_org_chart.py b/test/test_org_chart.py index 9ccefa4..bc3741e 100644 --- a/test/test_org_chart.py +++ b/test/test_org_chart.py @@ -73,7 +73,11 @@ def org_tree(client, test_user, test_user1, session, admin_headers): class TestAdminGetFullOrgChart: """GET /api/v1/users/org-chart (admin only)""" - def test_returns_roots(self, client, test_user, test_user1, admin_headers): + def test_returns_roots(self, client, test_user, test_user1, admin_headers, session): + # Make users ACCEPTED so they appear in org chart + _make_admin_accepted(session, test_user) + _make_admin_accepted(session, test_user1) + res = client.get("/api/v1/users/org-chart", headers=admin_headers) assert res.status_code == 200 data = res.json() @@ -82,7 +86,11 @@ def test_returns_roots(self, client, test_user, test_user1, admin_headers): assert test_user["id"] in root_ids assert test_user1["id"] in root_ids - def test_returns_tree_structure(self, client, org_tree, admin_headers): + def test_returns_tree_structure(self, client, org_tree, admin_headers, session): + # Make users ACCEPTED so they appear in org chart + _make_admin_accepted(session, org_tree["root"]) + _make_admin_accepted(session, org_tree["child"]) + res = client.get("/api/v1/users/org-chart", headers=admin_headers) assert res.status_code == 200 data = res.json() @@ -144,7 +152,11 @@ def test_forbidden_for_non_admin(self, client, test_user, user_headers): class TestAdminGetUserOrgChart: """GET /api/v1/users/{user_id}/org-chart (admin only)""" - def test_returns_subtree(self, client, org_tree, admin_headers): + def test_returns_subtree(self, client, org_tree, admin_headers, session): + # Make users ACCEPTED so they appear in org chart + _make_admin_accepted(session, org_tree["root"]) + _make_admin_accepted(session, org_tree["child"]) + res = client.get( f"/api/v1/users/{org_tree['root']['id']}/org-chart", headers=admin_headers, @@ -155,7 +167,11 @@ def test_returns_subtree(self, client, org_tree, admin_headers): sub_ids = {s["id"] for s in data["subordinates"]} assert org_tree["child"]["id"] in sub_ids - def test_leaf_node_subtree(self, client, org_tree, admin_headers): + def test_leaf_node_subtree(self, client, org_tree, admin_headers, session): + # Make users ACCEPTED so they appear in org chart + _make_admin_accepted(session, org_tree["root"]) + _make_admin_accepted(session, org_tree["child"]) + res = client.get( f"/api/v1/users/{org_tree['child']['id']}/org-chart", headers=admin_headers, @@ -317,7 +333,10 @@ def test_view_user_subordinates(self, client, org_tree, accepted_user_headers, s assert org_tree["child"]["id"] in sub_ids def test_view_user_org_chart(self, client, org_tree, accepted_user_headers, session): + # Make both users ACCEPTED so they appear in org chart + _make_admin_accepted(session, org_tree["root"]) _make_admin_accepted(session, org_tree["child"]) + res = client.get( f"/api/v1/users/view/{org_tree['root']['id']}/org-chart", headers=accepted_user_headers, diff --git a/test/test_profile_page.py b/test/test_profile_page.py index b83e975..325b77a 100644 --- a/test/test_profile_page.py +++ b/test/test_profile_page.py @@ -154,7 +154,14 @@ def test_get_user_info(client, test_user): assert "phone_number" in response.json()["data"] -def test_get_all_profile(client, test_user, test_users, test_stacks, populate_skills): +def test_get_all_profile(client, test_user, test_users, test_stacks, populate_skills, session): + # Set test_user to ACCEPTED status so they appear in active directory + from db.models.users import User + from utils.enums import UserStatus + user = session.query(User).filter(User.id == test_user["id"]).first() + user.status = UserStatus.ACCEPTED + session.commit() + login_res = client.post( "/api/v1/users/login", data={"username": test_user["email"], "password": test_user["password"]}) token = login_res.json()["token"] diff --git a/test/test_project_manager_validation.py b/test/test_project_manager_validation.py new file mode 100644 index 0000000..5b654e2 --- /dev/null +++ b/test/test_project_manager_validation.py @@ -0,0 +1,166 @@ +"""Tests for Project service manager validation""" +import pytest +from unittest.mock import Mock +from fastapi import HTTPException +from api.api_models.projects import UpdateProject +from services.project_service import ProjectService +from db.repository.projects import ProjectRepository +from db.repository.users import UserRepository +from db.repository.skills import SkillRepository +from db.repository.stacks import StackRepository + + +class TestProjectManagerValidation: + """Test project manager assignment and validation""" + + def test_update_manager_validates_manager_exists(self): + """Updating project manager should validate manager exists""" + project_repo = Mock(spec=ProjectRepository) + user_repo = Mock(spec=UserRepository) + skill_repo = Mock(spec=SkillRepository) + stack_repo = Mock(spec=StackRepository) + + service = ProjectService(project_repo, user_repo, stack_repo, skill_repo) + + # Mock existing project + mock_project = Mock() + mock_project.id = 1 + mock_project.manager_id = 1 + project_repo.get_by_id = Mock(return_value=mock_project) + + # Mock user_repo to return None (manager doesn't exist) + user_repo.get_by_id = Mock(return_value=None) + + update_data = UpdateProject(manager_id=999, name=None, description=None, project_type=None, project_priority=None, project_tools=None) + + with pytest.raises(HTTPException) as exc_info: + service.update_project(1, update_data) + + assert exc_info.value.status_code == 404 + assert "Manager not found" in exc_info.value.detail + + def test_update_manager_skips_validation_when_same(self): + """Updating manager to same value should skip existence check""" + project_repo = Mock(spec=ProjectRepository) + user_repo = Mock(spec=UserRepository) + skill_repo = Mock(spec=SkillRepository) + stack_repo = Mock(spec=StackRepository) + + service = ProjectService(project_repo, user_repo, stack_repo, skill_repo) + + # Mock existing project with manager_id = 5 + mock_project = Mock() + mock_project.id = 1 + mock_project.manager_id = 5 + mock_project.members = [] # Add empty members list for _enrich_project_members_with_team + project_repo.get_by_id = Mock(return_value=mock_project) + + # Mock update to return project + project_repo.update = Mock(return_value=mock_project) + + update_data = UpdateProject(manager_id=5, name=None, description=None, project_type=None, project_priority=None, project_tools=None) + + # Should not raise error - validation skipped when same manager + service.update_project(1, update_data) + + # user_repo.get_by_id should NOT be called since manager is same + user_repo.get_by_id.assert_not_called() + + def test_update_manager_success_with_valid_manager(self): + """Updating project manager should succeed with valid manager""" + project_repo = Mock(spec=ProjectRepository) + user_repo = Mock(spec=UserRepository) + skill_repo = Mock(spec=SkillRepository) + stack_repo = Mock(spec=StackRepository) + + service = ProjectService(project_repo, user_repo, stack_repo, skill_repo) + + # Mock existing project + mock_project = Mock() + mock_project.id = 1 + mock_project.manager_id = 1 + mock_project.members = [] + project_repo.get_by_id = Mock(return_value=mock_project) + + # Mock valid manager exists + mock_manager = Mock() + mock_manager.id = 5 + user_repo.get_by_id = Mock(return_value=mock_manager) + + # Mock update to return updated project + updated_project = Mock() + updated_project.id = 1 + updated_project.manager_id = 5 + updated_project.members = [] # Add empty members list for _enrich_project_members_with_team + project_repo.update = Mock(return_value=updated_project) + + update_data = UpdateProject(manager_id=5, name=None, description=None, project_type=None, project_priority=None, project_tools=None) + + result = service.update_project(1, update_data) + + # Should succeed + assert result is not None + user_repo.get_by_id.assert_called_with(5) + + def test_update_project_not_found(self): + """Updating non-existent project should raise 404""" + project_repo = Mock(spec=ProjectRepository) + user_repo = Mock(spec=UserRepository) + skill_repo = Mock(spec=SkillRepository) + stack_repo = Mock(spec=StackRepository) + + service = ProjectService(project_repo, user_repo, stack_repo, skill_repo) + + # Mock project doesn't exist + project_repo.get_by_id = Mock(return_value=None) + + update_data = UpdateProject(manager_id=1, name=None, description=None, project_type=None, project_priority=None, project_tools=None) + + with pytest.raises(HTTPException) as exc_info: + service.update_project(999, update_data) + + assert exc_info.value.status_code == 404 + assert "Project not found" in exc_info.value.detail + + def test_update_exclude_none_values(self): + """Update should only send non-None fields to repository""" + project_repo = Mock(spec=ProjectRepository) + user_repo = Mock(spec=UserRepository) + skill_repo = Mock(spec=SkillRepository) + stack_repo = Mock(spec=StackRepository) + + service = ProjectService(project_repo, user_repo, stack_repo, skill_repo) + + # Mock existing project + mock_project = Mock() + mock_project.id = 1 + mock_project.manager_id = 1 + mock_project.members = [] # Add empty members list for _enrich_project_members_with_team + project_repo.get_by_id = Mock(return_value=mock_project) + project_repo.update = Mock(return_value=mock_project) + + # Update with only name (others None) + update_data = UpdateProject( + name="New Name", + description=None, + project_type=None, + project_priority=None, + manager_id=None, + project_tools=None + ) + + service.update_project(1, update_data) + + # Verify project_repo.update was called + project_repo.update.assert_called_once() + + # Get the actual call arguments + call_args = project_repo.update.call_args + update_dict = call_args[0][1] # Second argument is the update dict + + # Verify only non-None fields are in update dict + assert "name" in update_dict + assert update_dict["name"] == "New Name" + # None fields should not be in the dict due to exclude_none=True + if "description" in update_dict: + assert update_dict["description"] is not None diff --git a/test/test_projects.py b/test/test_projects.py index 7190533..89a562c 100644 --- a/test/test_projects.py +++ b/test/test_projects.py @@ -171,12 +171,20 @@ def test_add_user_project(client, test_projects, user_cred, test_user1): assert res.status_code == status.HTTP_201_CREATED -def test_add_user_project_unauthorized(client, test_projects, test_user, user_cred): - url = project_url + str(test_projects[2].id) + "/add/" + str(test_user["id"]) +def test_add_user_project_unauthorized(client, test_projects, test_user, test_user1): + # test_user1 (non-admin) trying to add a user to test_projects[0] (managed by test_user) + # This should be forbidden since test_user1 is not the manager and not an admin + login_res = client.post( + "/api/v1/users/login", + data={"username": test_user1["email"], "password": test_user1["password"]} + ) + token = login_res.json()["token"] + + url = project_url + str(test_projects[0].id) + "/add/" + str(test_user["id"]) data = { "team": "FRONTEND" } - res = client.post(url, json=data, headers={"Authorization": f"{user_cred.token_type} {user_cred.token}"}) + res = client.post(url, json=data, headers={"Authorization": f"Bearer {token}"}) assert res.status_code == status.HTTP_403_FORBIDDEN @@ -211,10 +219,21 @@ def test_remove_user_project(client, test_projects, user_cred, test_user1): assert res.status_code == status.HTTP_204_NO_CONTENT -def test_remove_user_project_unauthorized(client, test_projects, test_user, user_cred): - url = project_url + str(test_projects[2].id) + "/remove/" + str(test_user["id"]) +def test_remove_user_project_unauthorized(client, test_projects, test_user, test_user1, user_cred): + # First, add test_user to test_projects[0] (managed by test_user, who is admin) + test_add_user_project(client, test_projects, user_cred, test_user1) - res = client.delete(url, headers={"Authorization": f"{user_cred.token_type} {user_cred.token}"}) + # Now test_user1 (non-admin) tries to remove from test_projects[0] (managed by test_user) + # This should be forbidden since test_user1 is not the manager and not an admin + login_res = client.post( + "/api/v1/users/login", + data={"username": test_user1["email"], "password": test_user1["password"]} + ) + token = login_res.json()["token"] + + url = project_url + str(test_projects[0].id) + "/remove/" + str(test_user1["id"]) + + res = client.delete(url, headers={"Authorization": f"Bearer {token}"}) assert res.status_code == status.HTTP_403_FORBIDDEN diff --git a/test/test_skill_service_response.py b/test/test_skill_service_response.py new file mode 100644 index 0000000..4c0db39 --- /dev/null +++ b/test/test_skill_service_response.py @@ -0,0 +1,105 @@ +"""Tests for Skill service response format""" +import pytest +from unittest.mock import Mock +from services.skill_service import SkillService +from db.repository.skills import SkillRepository + + +class TestSkillServiceResponseFormat: + """Test that search_skills returns consistent response format""" + + def test_search_skills_returns_correct_field_names(self): + """search_skills should return skill_id, skill_name, image_url""" + skill_repo = Mock(spec=SkillRepository) + service = SkillService(skill_repo) + + # Mock skill objects + mock_skill = Mock() + mock_skill.id = 1 + mock_skill.name = "Python" + mock_skill.image_url = "https://example.com/python.png" + + skill_repo.get_all_flat = Mock(return_value=[mock_skill]) + + result = service.search_skills("python") + + # Verify response is list of dicts + assert len(result) > 0 + assert isinstance(result, list) + assert isinstance(result[0], dict) + + # Verify field names + assert "skill_id" in result[0] + assert "skill_name" in result[0] + assert "image_url" in result[0] + + def test_search_skills_maps_model_fields_correctly(self): + """search_skills should map skill model id→skill_id, name→skill_name""" + skill_repo = Mock(spec=SkillRepository) + service = SkillService(skill_repo) + + mock_skill = Mock() + mock_skill.id = 5 + mock_skill.name = "React" + mock_skill.image_url = "https://example.com/react.png" + + skill_repo.get_all_flat = Mock(return_value=[mock_skill]) + + result = service.search_skills("react") + + assert result[0]["skill_id"] == 5 + assert result[0]["skill_name"] == "React" + assert result[0]["image_url"] == "https://example.com/react.png" + + def test_search_skills_handles_null_image_url(self): + """search_skills should return empty string when image_url is None""" + skill_repo = Mock(spec=SkillRepository) + service = SkillService(skill_repo) + + mock_skill = Mock() + mock_skill.id = 1 + mock_skill.name = "JavaScript" + mock_skill.image_url = None + + skill_repo.get_all_flat = Mock(return_value=[mock_skill]) + + result = service.search_skills("javascript") + + # Should have empty string, not None + assert result[0]["image_url"] == "" + + def test_search_skills_fuzzy_matching(self): + """search_skills should perform fuzzy matching with threshold""" + from unittest.mock import MagicMock + + skill_repo = Mock(spec=SkillRepository) + service = SkillService(skill_repo) + + # Create Mock skills - use actual strings for name since fuzzy matching needs real strings + skill1 = Mock() + skill1.id = 1 + skill1.name = "Python" + skill1.image_url = "url1" + + skill2 = Mock() + skill2.id = 2 + skill2.name = "JavaScript" + skill2.image_url = "url2" + + skill3 = Mock() + skill3.id = 3 + skill3.name = "TypeScript" + skill3.image_url = "url3" + + skills = [skill1, skill2, skill3] + + skill_repo.get_all_flat = Mock(return_value=skills) + + # Search for "java" should match both Java and JavaScript + result = service.search_skills("java") + + # Should find JavaScript at minimum + assert len(result) > 0 + # Should contain JavaScript + found_js = any(s["skill_name"] == "JavaScript" for s in result) + assert found_js diff --git a/test/test_technical_task_submissions.py b/test/test_technical_task_submissions.py new file mode 100644 index 0000000..2ee0dd1 --- /dev/null +++ b/test/test_technical_task_submissions.py @@ -0,0 +1,134 @@ +"""Tests for Technical Task Submissions API and service""" +import pytest +from datetime import datetime +from unittest.mock import Mock, MagicMock, patch +from sqlalchemy.orm import Session + +from db.models.technical_task import TechnicalTaskSubmission, TechnicalTask +from db.models.users import User +from db.models.stacks import Stack +from services.technical_task_service import TechnicalTaskService +from db.repository.technical_tasks import TechnicalTaskRepository, TechnicalTaskSubmissionRepository + + +class TestTechnicalTaskSubmissionResponse: + """Test that submission responses include nested user and task data""" + + def test_submission_response_includes_user_data(self): + """Submission response should include user details""" + user = Mock(spec=User) + user.id = 1 + user.first_name = "John" + user.last_name = "Doe" + user.username = "johndoe" + user.profile_pic_url = "https://example.com/pic.jpg" + + submission = Mock(spec=TechnicalTaskSubmission) + submission.id = 1 + submission.user = user + submission.github_link = "https://github.com/johndoe/project" + submission.live_demo_url = "https://project.example.com" + submission.description = "My submission" + submission.created_at = datetime.now() + submission.updated_at = datetime.now() + submission.task_id = 1 + + # Verify user data is accessible + assert submission.user.id == 1 + assert submission.user.first_name == "John" + assert submission.user.last_name == "Doe" + assert submission.user.username == "johndoe" + assert submission.user.profile_pic_url is not None + + def test_submission_response_includes_task_data(self): + """Submission response should include technical task details with stack""" + stack = Mock(spec=Stack) + stack.id = 1 + stack.name = "Backend" + + task = Mock(spec=TechnicalTask) + task.id = 1 + task.content = "Build a REST API" + task.experience_level = "JUNIOR" + task.stack = stack + + submission = Mock(spec=TechnicalTaskSubmission) + submission.id = 1 + submission.technical_task = task + submission.task_id = 1 + + # Verify task data is accessible + assert submission.technical_task.id == 1 + assert submission.technical_task.content == "Build a REST API" + assert submission.technical_task.experience_level == "JUNIOR" + assert submission.technical_task.stack.name == "Backend" + + def test_submission_response_has_all_required_fields(self): + """Submission response should have all submission metadata fields""" + submission = Mock(spec=TechnicalTaskSubmission) + submission.id = 1 + submission.github_link = "https://github.com/user/repo" + submission.live_demo_url = "https://demo.example.com" + submission.description = "My work" + submission.created_at = datetime.now() + submission.updated_at = datetime.now() + submission.task_id = 1 + submission.user_id = 1 + submission.user = Mock() + submission.technical_task = Mock() + + # Verify all fields present + assert hasattr(submission, 'id') + assert hasattr(submission, 'github_link') + assert hasattr(submission, 'live_demo_url') + assert hasattr(submission, 'description') + assert hasattr(submission, 'created_at') + assert hasattr(submission, 'updated_at') + assert hasattr(submission, 'task_id') + assert hasattr(submission, 'user_id') + assert hasattr(submission, 'user') + assert hasattr(submission, 'technical_task') + + +class TestTechnicalTaskServiceErrorHandling: + """Test error handling in technical task service""" + + def test_create_submission_provides_context_on_task_not_found(self): + """When task not found, error message should include stack_id and experience_level""" + task_repo = Mock(spec=TechnicalTaskRepository) + submission_repo = Mock(spec=TechnicalTaskSubmissionRepository) + service = TechnicalTaskService(task_repo, submission_repo) + + user = Mock(spec=User) + user.id = 1 + user.stack_id = 5 + user.years_of_experience = 0 + + # Mock task_repo to return None + task_repo.get_by_stack_and_level = Mock(return_value=None) + + # Service should provide context in error + from fastapi import HTTPException + + with pytest.raises(HTTPException) as exc_info: + service.create_submission(user, {"github_link": "test"}) + + error_detail = exc_info.value.detail + # Error message should mention stack_id and experience level + assert "stack_id=5" in error_detail or "JUNIOR" in error_detail + + def test_create_submission_error_on_invalid_experience(self): + """Creating submission should raise error when years_of_experience is invalid""" + task_repo = Mock(spec=TechnicalTaskRepository) + submission_repo = Mock(spec=TechnicalTaskSubmissionRepository) + service = TechnicalTaskService(task_repo, submission_repo) + + user = Mock(spec=User) + user.id = 1 + user.stack_id = 1 + user.years_of_experience = 999 # Invalid value + + from fastapi import HTTPException + + with pytest.raises(HTTPException): + service.create_submission(user, {"github_link": "test"}) diff --git a/utils/email_templates/password-reset.html b/utils/email_templates/password-reset.html index f29c32a..66c196c 100644 --- a/utils/email_templates/password-reset.html +++ b/utils/email_templates/password-reset.html @@ -3,9 +3,10 @@ Password Reset Email + - + - - +
+ +
SlightlyTechieLogo - - - - - - +

ST Network

+ + + + + +
-

- Hi {} -

-

- You are receiving this mail because you requested for a password reset for your account on Slightly Techie CRM -

-

Kindly click the button below to reset your password

- Reset Your Password -

- If you are not aware of this request, please disregard this - email. -

-

- Contact us on - info@slightlytechie.com for more - information -

-
+

+ Hi {}! +

+ +

+ We received a request to reset the password for your Slightly Techie CRM account. To proceed with resetting your password, click the button below. +

+ + + + + +
+ Reset Your Password +
+ +

+ This link will expire in 24 hours. If you didn't request a password reset, you can safely ignore this email. +

+ + +
+ + +

+ Have questions? Contact us at + info@slightlytechie.com +

+ +

+ © 2026 Slightly Techie. All rights reserved. +

diff --git a/utils/email_templates/task_template.html b/utils/email_templates/task_template.html index 5f9c6df..36ec7f2 100644 --- a/utils/email_templates/task_template.html +++ b/utils/email_templates/task_template.html @@ -2,10 +2,11 @@ - Password Reset Email + New Task Assignment + - + - - +
+ +
SlightlyTechieLogo - - - - + + + + +
-

- Hi {} -

-

- {} +

ST Network

+
+

+ Hi {}! +

+ +

+ {} +

+ + + + + - - + +
+

Next Steps

+

+ Sign in to app.slightlytechie.com to view and submit your task.

-

Kindly sign in to app.slightlytechie.com to submit your task when completed.

- -

-

-

- Contact us on - info@slightlytechie.com for more - information -

-
+ + + + + + +
+ Go to Dashboard +
+ +

+ If you have any questions about this task, please reach out to your team lead or contact support. +

+ + +
+ + +

+ Have questions? Contact us at + info@slightlytechie.com +

+ +

+ © 2026 Slightly Techie. All rights reserved. +

diff --git a/utils/oauth2.py b/utils/oauth2.py index c751a78..bbbf301 100644 --- a/utils/oauth2.py +++ b/utils/oauth2.py @@ -74,7 +74,10 @@ def verify_refresh_token(token: str): def get_current_user( token: str = Depends(oauth2_scheme), db: Session = Depends(database.get_db)): - + """ + Base authentication: verifies token and returns user. + Does NOT check is_active or status - use permission dependencies for that. + """ credential_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", @@ -92,11 +95,6 @@ def get_current_user( print(f"User with ID {token_data.id} not found in database.") raise credential_exception - if not user.is_active: - if user.status == "CONTACTED": - return user - # Redirect the user to a default URL - raise HTTPException(status_code=302, headers={"Location": "/inactive"}) return user diff --git a/utils/permissions.py b/utils/permissions.py index 76d6156..e779aba 100644 --- a/utils/permissions.py +++ b/utils/permissions.py @@ -12,7 +12,7 @@ def is_authenticated(user: UserResponse = Depends(get_current_user)): """ No need to do anything because `get_current_user` raises all the errors - his would be used as a path dependecy so return value is required + This would be used as a path dependency so return value is required Usage: @app.("", dependencies=[Depends(is_authenticated)] ) """ @@ -33,6 +33,10 @@ def is_project_manager( db: Session = Depends(get_db), user: UserResponse = Depends(get_current_user), ): + # Allow admins + if user.role.name == RoleChoices.ADMIN: + return user + project_id = request.path_params.get("project_id") if project_id is None: raise HTTPException(status_code=500, detail="project_id path parameter missing") @@ -78,10 +82,35 @@ def is_owner(user, obj): def user_accepted(user: UserResponse = Depends(get_current_user)): - """Only accepted users can access feeds page""" - if user.status != UserStatus.ACCEPTED: + """Only active and accepted users can access protected resources""" + if not user.is_active or user.status != UserStatus.ACCEPTED: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You do not have access to this resource" ) return user + + +def user_contacted(user: UserResponse = Depends(get_current_user)): + """Only CONTACTED users (for task submission)""" + if user.status != UserStatus.CONTACTED: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="This resource is only for applicants with tasks" + ) + return user + + +def user_active_and_accepted(user: UserResponse = Depends(get_current_user)): + """Strict check: user must be both active AND accepted""" + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Your account is not active. Please contact an administrator." + ) + if user.status != UserStatus.ACCEPTED: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Your application is still being processed." + ) + return user diff --git a/utils/utils.py b/utils/utils.py index fd45c03..0f2065c 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -33,10 +33,13 @@ class RoleChoices(): def get_key_by_value(value): + # Map years of experience to ExperienceLevel enum values + from utils.enums import ExperienceLevel + experience_level_map = { - "JUNIOR": [0, 1, 2], - "MID_LEVEL": [3, 4], - "SENIOR": [5, 6, 7, 8, 9, 10, 11, 12] + ExperienceLevel.JUNIOR.value: [0, 1, 2], + ExperienceLevel.MID_LEVEL.value: [3, 4], + ExperienceLevel.SENIOR.value: [5, 6, 7, 8, 9, 10, 11, 12] } for key, values in experience_level_map.items(): if value in values: From 2df7b23d0b811f9cc5cabc4e5ccc6b0574fb11d3 Mon Sep 17 00:00:00 2001 From: jbkyei1 Date: Fri, 3 Apr 2026 00:23:15 +0000 Subject: [PATCH 2/2] feat: Improve validation and error handling for user experience levels and project permissions --- api/api_models/technical_task.py | 2 +- api/routes/project.py | 6 +++--- services/org_chart_service.py | 15 +++++++++++++-- services/project_service.py | 28 +++++++++++++++++++--------- services/skill_service.py | 19 +++++++++++++------ services/technical_task_service.py | 9 +++++++++ utils/permissions.py | 29 ++--------------------------- utils/utils.py | 5 ++++- 8 files changed, 64 insertions(+), 49 deletions(-) diff --git a/api/api_models/technical_task.py b/api/api_models/technical_task.py index b01aef7..9c6cc10 100644 --- a/api/api_models/technical_task.py +++ b/api/api_models/technical_task.py @@ -22,7 +22,7 @@ class TechnicalTaskResponse(TechnicalTaskBase): class TechnicalTaskSubmissionBase(BaseModel): - github_link: Optional[Text] = None + github_link: Text = Field(...) live_demo_url: Optional[Text] = None description: Optional[Text] = None diff --git a/api/routes/project.py b/api/routes/project.py index 30bc38b..8668199 100644 --- a/api/routes/project.py +++ b/api/routes/project.py @@ -51,11 +51,11 @@ def get(project_id: int, db: Session = Depends(get_db)): @project_router.get("/", status_code=status.HTTP_200_OK, response_model=Page[ProjectResponse]) def get_all(db: Session = Depends(get_db)): - - page = paginate(db, _service(db).get_all_query()) + service = _service(db) + page = paginate(db, service.get_all_query()) # Enrich members with team data for project in page.items: - _service(db)._enrich_project_members_with_team(project) + service._enrich_project_members_with_team(project) return page diff --git a/services/org_chart_service.py b/services/org_chart_service.py index 32c5622..0798398 100644 --- a/services/org_chart_service.py +++ b/services/org_chart_service.py @@ -5,6 +5,7 @@ from api.api_models.user import OrgChartNode from db.models.users import User from db.repository.org_chart import OrgChartRepository +from utils.enums import UserStatus class OrgChartService: @@ -100,7 +101,17 @@ def get_subtree(self, root_id: int, max_depth: Optional[int] = None, filter_acti user_ids = [r["id"] for r in rows] users = self.repo.get_users_by_ids(user_ids) users_by_id = {u.id: u for u in users} - return self._build_tree_filtered(users_by_id, root_id, rows, filter_active) + tree_node = self._build_tree_filtered(users_by_id, root_id, rows, filter_active) + if tree_node is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=( + f"User with id {root_id} is not available in the filtered org chart" + if filter_active + else f"Unable to build org chart subtree for user with id {root_id}" + ), + ) + return tree_node def get_full_org_chart(self, max_depth: Optional[int] = None, filter_active: bool = True) -> list[OrgChartNode]: """Get the full organizational chart. @@ -144,7 +155,7 @@ def _recurse(uid: int) -> Optional[OrgChartNode]: u = users_by_id[uid] # Skip if filtering is enabled and user doesn't match - if filter_active and (u.status != "ACCEPTED" or not u.is_active): + if filter_active and (u.status != UserStatus.ACCEPTED or not u.is_active): return None child_ids = children_map.get(uid, []) diff --git a/services/project_service.py b/services/project_service.py index aa4519f..f86d9d4 100644 --- a/services/project_service.py +++ b/services/project_service.py @@ -23,14 +23,19 @@ def __init__(self, project_repo: ProjectRepository, user_repo: UserRepository, def _enrich_project_members_with_team(self, project: Project) -> Project: """Add team role data from users_projects join table to project members.""" + if not project.members: + return project + + user_projects = self.project_repo.db.query(UserProject).filter( + UserProject.project_id == project.id + ).all() + teams_by_user_id = {user_project.user_id: user_project.team for user_project in user_projects} + for member in project.members: - user_project = self.project_repo.db.query(UserProject).filter( - UserProject.user_id == member.id, - UserProject.project_id == project.id - ).first() - if user_project: + team = teams_by_user_id.get(member.id) + if team is not None: # Dynamically set the team attribute for Pydantic serialization - object.__setattr__(member, 'team', user_project.team) + setattr(member, 'team', team) return project def create_project(self, project_data: CreateProject) -> Project: @@ -38,12 +43,17 @@ def create_project(self, project_data: CreateProject) -> Project: if not manager: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Manager not found") + # Members are added separately via /add/{userId} endpoint, not during creation + if project_data.members: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Members cannot be added during project creation. " + "Use the POST /projects/{project_id}/add/{user_id} endpoint to add members with their team roles." + ) + data = project_data.model_dump(exclude=["members", "stacks", "project_tools"]) new_project = Project(**data) - # Note: Members are NOT added here. They are added separately via the /add/{userId} endpoint - # with their team roles specified. This ensures the users_projects join table has the team field set. - if project_data.stacks: seen = [] for stack_id in project_data.stacks: diff --git a/services/skill_service.py b/services/skill_service.py index b6ba10d..b08ea1e 100644 --- a/services/skill_service.py +++ b/services/skill_service.py @@ -45,18 +45,25 @@ def populate_skills(self) -> dict: from db.database import create_roles from utils.endpoints_status import create_signup_endpoint from utils.tools import tools as skills_data, get_skills_image + import logging - # Try to create roles and signup endpoint, but don't fail if they already exist - # or if running in a test environment with different database session + logger = logging.getLogger(__name__) + + # Try to create roles - catch expected "already exists" exceptions try: create_roles() - except Exception: - pass # Roles likely already exist + except Exception as e: + # Expected when roles already exist; log unexpected errors for diagnostics + if "already" not in str(e).lower(): + logger.warning(f"Unexpected error creating roles: {e}") + # Try to create signup endpoint - catch expected "already exists" exceptions try: create_signup_endpoint(status=True) - except Exception: - pass # Endpoint likely already exists + except Exception as e: + # Expected when endpoint already exists; log unexpected errors for diagnostics + if "already" not in str(e).lower(): + logger.warning(f"Unexpected error creating signup endpoint: {e}") try: last_skill = None diff --git a/services/technical_task_service.py b/services/technical_task_service.py index 64f0fd6..72d7aab 100644 --- a/services/technical_task_service.py +++ b/services/technical_task_service.py @@ -75,6 +75,15 @@ def create_submission(self, current_user: User, data: dict) -> TechnicalTaskSubm status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) + + if user_exp is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"Invalid years_of_experience value: {current_user.years_of_experience}. " + f"Please update your profile with a valid experience level before submitting." + ) + ) task = self.task_repo.get_by_stack_and_level(current_user.stack_id, user_exp) if not task: diff --git a/utils/permissions.py b/utils/permissions.py index e779aba..4e98aee 100644 --- a/utils/permissions.py +++ b/utils/permissions.py @@ -22,7 +22,7 @@ def is_authenticated(user: UserResponse = Depends(get_current_user)): # Admin permission dependency def is_admin(user: UserResponse = Depends(is_authenticated)): - if user.role.name != RoleChoices.ADMIN: + if not user.role or user.role.name != RoleChoices.ADMIN: raise ForbiddenError() return user @@ -34,7 +34,7 @@ def is_project_manager( user: UserResponse = Depends(get_current_user), ): # Allow admins - if user.role.name == RoleChoices.ADMIN: + if user.role and user.role.name == RoleChoices.ADMIN: return user project_id = request.path_params.get("project_id") @@ -89,28 +89,3 @@ def user_accepted(user: UserResponse = Depends(get_current_user)): detail="You do not have access to this resource" ) return user - - -def user_contacted(user: UserResponse = Depends(get_current_user)): - """Only CONTACTED users (for task submission)""" - if user.status != UserStatus.CONTACTED: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="This resource is only for applicants with tasks" - ) - return user - - -def user_active_and_accepted(user: UserResponse = Depends(get_current_user)): - """Strict check: user must be both active AND accepted""" - if not user.is_active: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Your account is not active. Please contact an administrator." - ) - if user.status != UserStatus.ACCEPTED: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Your application is still being processed." - ) - return user diff --git a/utils/utils.py b/utils/utils.py index 0f2065c..9bb552a 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -44,4 +44,7 @@ def get_key_by_value(value): for key, values in experience_level_map.items(): if value in values: return key - return None + raise ValueError( + f"Invalid years_of_experience value: {value}. " + f"Must be between 0 and 12. Valid ranges: Junior (0-2), Mid-level (3-4), Senior (5-12)." + )