Skip to content
Merged
Show file tree
Hide file tree
Changes from 92 commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
b6721b6
program mutation and node
Rajgupta36 Jun 30, 2025
e9ac12d
UI for program create , view and edit
Rajgupta36 Jul 1, 2025
b58802f
backend completed
Rajgupta36 Jul 3, 2025
abf873b
create module edit and view page
Rajgupta36 Jul 6, 2025
79b6268
type fix
Rajgupta36 Jul 6, 2025
eb8f076
update components
Rajgupta36 Jul 6, 2025
d6231c0
updated backend
Rajgupta36 Jul 6, 2025
fbb82ea
slugify urls
Rajgupta36 Jul 6, 2025
8d571fc
pre commit
Rajgupta36 Jul 6, 2025
468f0b8
fix components
Rajgupta36 Jul 6, 2025
a3c5d3b
fix merge conflicts
Rajgupta36 Jul 6, 2025
dbed9b9
update suggestions
Rajgupta36 Jul 7, 2025
be106ca
Merge branch 'main' into feat/mentorship-dev
Rajgupta36 Jul 7, 2025
ca3d122
update pnpm-lock file
Rajgupta36 Jul 7, 2025
ad1d76f
fix spell check
Rajgupta36 Jul 7, 2025
779df27
fix publish program
Rajgupta36 Jul 7, 2025
fb951f6
fix unit test cases
Rajgupta36 Jul 7, 2025
93236e4
Merge branch 'main' into feat/mentorship-dev
kasya Jul 8, 2025
c285a54
fix pre-commits
Rajgupta36 Jul 8, 2025
d2f0864
update frontend logic
Rajgupta36 Jul 9, 2025
a98cd10
format
Rajgupta36 Jul 9, 2025
471d502
refactor code
Rajgupta36 Jul 11, 2025
d0e8c7a
fix terminal
Rajgupta36 Jul 11, 2025
c96ed0b
suggestion fixes
Rajgupta36 Jul 11, 2025
3c1a25a
added unit test cases
Rajgupta36 Jul 12, 2025
99474f3
fix test cases
Rajgupta36 Jul 12, 2025
028e6b9
optimization
Rajgupta36 Jul 12, 2025
53035d1
Merge branch 'main' into feat/mentorship-dev
Rajgupta36 Jul 12, 2025
668b0ea
generated code
Rajgupta36 Jul 12, 2025
8844c2d
update code
Rajgupta36 Jul 12, 2025
fe3ff9e
update code
Rajgupta36 Jul 12, 2025
0b09822
update naming,validation logic
Rajgupta36 Jul 13, 2025
454cd66
Merge branch 'main' into feat/mentorship-dev
Rajgupta36 Jul 13, 2025
94cceed
cleanup
Rajgupta36 Jul 13, 2025
4d56bcf
fix project suggestion bug
Rajgupta36 Jul 14, 2025
1579788
Merge branch 'main' into feat/mentorship-dev
Rajgupta36 Jul 14, 2025
0468fcf
update code
Rajgupta36 Jul 14, 2025
d5c4c91
merge main into feat/mentorship-dev
Rajgupta36 Jul 15, 2025
3a1ee00
fixes
Rajgupta36 Jul 15, 2025
8d5e9fd
apply suggestions
Rajgupta36 Jul 16, 2025
3cbb44c
merge main into feat/mentorship-dev
Rajgupta36 Jul 16, 2025
32962c9
update suggestion
Rajgupta36 Jul 16, 2025
d1d89ab
merge main into feat/mentorship-dev
Rajgupta36 Jul 17, 2025
a67a3fb
update code
Rajgupta36 Jul 18, 2025
aa42c75
merge main into feat/mentorship-dev
Rajgupta36 Jul 18, 2025
45fe1e0
merge main into feat/mentorship-dev
Rajgupta36 Jul 18, 2025
7596754
Merge branch 'main' into feat/mentorship-dev
kasya Jul 20, 2025
d806214
merge main into feat/mentorship-dev
Rajgupta36 Jul 22, 2025
8ba10b8
update code
Rajgupta36 Jul 22, 2025
61b3725
Merge branch 'main' into feat/mentorship-dev
Rajgupta36 Jul 22, 2025
30ff8e7
update frozen-lockfile
Rajgupta36 Jul 22, 2025
b518fc9
Merge branch 'main' into feat/mentorship-dev
kasya Jul 23, 2025
8eb6522
merge main into feat/mentorship-dev
Rajgupta36 Jul 24, 2025
81b48aa
Merge remote-tracking branch 'upstream/main' into feat/mentorship-dev
Rajgupta36 Jul 24, 2025
478d3cf
update code and make program page view only
Rajgupta36 Jul 26, 2025
4ddf608
updated migration and add query
Rajgupta36 Jul 26, 2025
8b4e085
added index
Rajgupta36 Jul 27, 2025
cb8665a
updated components, mutaitons and nodes
Rajgupta36 Jul 28, 2025
2e8fc2a
update code
Rajgupta36 Jul 28, 2025
7197275
added constraints
Rajgupta36 Jul 28, 2025
63ecb63
fix test case
Rajgupta36 Jul 28, 2025
eb24e0b
merge main into feat/mentorship-dev
Rajgupta36 Jul 28, 2025
4b7d5dc
added unit test case for program page
Rajgupta36 Jul 28, 2025
8e7cb8f
merge main into feat/mentorship-dev
Rajgupta36 Jul 28, 2025
b3f51ec
added remaining unit test cases
Rajgupta36 Jul 28, 2025
5689e93
fix bug
Rajgupta36 Jul 28, 2025
b98893f
merge main into feat/mentorship-dev
Rajgupta36 Jul 29, 2025
f1d3b24
update code
Rajgupta36 Aug 1, 2025
27839f8
merge main into feat/mentorship-dev
Rajgupta36 Aug 1, 2025
a27a4d6
updated auth and projectleader code
Rajgupta36 Aug 2, 2025
ee84b4a
update test cases
Rajgupta36 Aug 2, 2025
d2ef0ce
update code
Rajgupta36 Aug 2, 2025
06bb530
revert test case
Rajgupta36 Aug 2, 2025
80a83c0
merge upstream into main
Rajgupta36 Aug 5, 2025
29afecd
fix unit test cases
Rajgupta36 Aug 5, 2025
04e060b
migration fix
Rajgupta36 Aug 6, 2025
0405292
merge upstream into main
Rajgupta36 Aug 6, 2025
f446ecf
increase test coverage
Rajgupta36 Aug 6, 2025
3e4c9e4
update suggestions
Rajgupta36 Aug 7, 2025
75ae1ba
added function for cache clear by index_name
Rajgupta36 Aug 7, 2025
865dbac
created component for single module
Rajgupta36 Aug 7, 2025
9e28465
fix typo
Rajgupta36 Aug 7, 2025
a9a734c
fix some frontend test cases
Rajgupta36 Aug 7, 2025
2f5e88d
added post save signal
Rajgupta36 Aug 8, 2025
3e51bec
added more test cases
Rajgupta36 Aug 8, 2025
1b6f6f4
merge upstream into main
Rajgupta36 Aug 8, 2025
95f824b
merge upstream into main and update singlemodulecard
Rajgupta36 Aug 8, 2025
7fe07d4
pnpm updated
Rajgupta36 Aug 8, 2025
e3b1c5b
increase coverage
Rajgupta36 Aug 8, 2025
14f5519
Merge branch 'main' into feat/mentorship-dev
kasya Aug 9, 2025
df539bc
update suggestion
Rajgupta36 Aug 9, 2025
9674fb0
updated backend suggestions
Rajgupta36 Aug 10, 2025
c522b1c
merge upstream into main
Rajgupta36 Aug 10, 2025
f66ddde
added isMentor query
Rajgupta36 Aug 10, 2025
376ccdb
fix page loading
Rajgupta36 Aug 10, 2025
dc97d9a
merge upstream into main
Rajgupta36 Aug 11, 2025
dd6f087
Update module description styling
kasya Aug 12, 2025
fb1f7e8
merge upstream into main
Rajgupta36 Aug 12, 2025
e4fa7a4
merge upstream into main
Rajgupta36 Aug 12, 2025
87fbdd2
Update code
arkid15r Aug 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/apps/core/api/internal/algolia.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@

from apps.common.index import IndexBase
from apps.common.utils import get_user_ip_address
from apps.core.constants import CACHE_PREFIX
from apps.core.utils.index import deep_camelize, get_params_for_index
from apps.core.validators import validate_search_params

CACHE_PREFIX = "algolia_proxy"
CACHE_TTL_IN_SECONDS = 3600 # 1 hour


Expand Down
3 changes: 3 additions & 0 deletions backend/apps/core/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Core app constants."""

CACHE_PREFIX = "algolia_proxy"
46 changes: 46 additions & 0 deletions backend/apps/core/utils/index.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"""Index utils."""

import contextlib
import logging

from algoliasearch_django import register, unregister
from algoliasearch_django.registration import RegistrationError
from django.apps import apps
from django.core.cache import cache

from apps.common.utils import convert_to_camel_case
from apps.core.constants import CACHE_PREFIX

logger = logging.getLogger(__name__)


class DisableIndexing:
Expand Down Expand Up @@ -140,6 +145,18 @@ def get_params_for_index(index_name: str) -> dict:
]
params["aroundLatLngViaIP"] = True

case "programs":
params["attributesToRetrieve"] = [
"idx_description",
"idx_ended_at",
"idx_experience_levels",
"idx_key",
"idx_modules",
"idx_name",
"idx_started_at",
"idx_status",
]

case "projects":
params["attributesToRetrieve"] = [
"idx_contributors_count",
Expand Down Expand Up @@ -213,3 +230,32 @@ def get_params_for_index(index_name: str) -> dict:
params["attributesToRetrieve"] = []

return params


def clear_index_cache(index_name: str) -> None:
"""Clear Algolia proxy cache entries from the cache store that match a given index name.

Args:
index_name (str): The specific index to clear cache for.
If empty, the function does nothing.

Returns:
None

"""
if not index_name:
logger.info("No index name provided, skipping cache clear.")
return

pattern = f"{CACHE_PREFIX}:{index_name}*"
keys_to_delete = list(cache.iter_keys(pattern))

if not keys_to_delete:
logger.info("No matching cache keys found for pattern: %s", pattern)
return

logger.info("Deleting %d cache keys for pattern: %s", len(keys_to_delete), pattern)

for key in keys_to_delete:
logger.info("Deleting key: %s", key)
cache.delete(key)
2 changes: 1 addition & 1 deletion backend/apps/mentorship/admin/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.contrib import admin

from apps.mentorship.models.program import Program
from apps.mentorship.models import Program


class ProgramAdmin(admin.ModelAdmin):
Expand Down
Empty file.
Empty file.
4 changes: 4 additions & 0 deletions backend/apps/mentorship/api/internal/mutations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Mentorship mutations."""

from .module import ModuleMutation
from .program import ProgramMutation
197 changes: 197 additions & 0 deletions backend/apps/mentorship/api/internal/mutations/module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"""GraphQL mutations for mentorship modules in the mentorship app."""

import logging

import strawberry
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
from django.db import transaction
from django.utils import timezone

from apps.github.models import User as GithubUser
from apps.mentorship.api.internal.nodes.module import (
CreateModuleInput,
ModuleNode,
UpdateModuleInput,
)
from apps.mentorship.models import Mentor, Module, Program
from apps.nest.api.internal.permissions import IsAuthenticated
from apps.owasp.models import Project

logger = logging.getLogger(__name__)


def resolve_mentors_from_logins(logins: list[str]) -> set[Mentor]:
"""Resolve a list of GitHub logins to a set of Mentor objects."""
mentors = set()
for login in logins:
try:
github_user = GithubUser.objects.get(login__iexact=login.lower())
mentor, _ = Mentor.objects.get_or_create(github_user=github_user)
mentors.add(mentor)
except GithubUser.DoesNotExist as e:
msg = f"GitHub user '{login}' not found."
logger.warning(msg, exc_info=True)
raise ValueError(msg) from e
return mentors


def _validate_module_dates(started_at, ended_at, program_started_at, program_ended_at) -> tuple:
"""Validate and normalize module start/end dates against program constraints."""
if started_at is None or ended_at is None:
msg = "Both start and end dates are required."
raise ValidationError(message=msg)

if timezone.is_naive(started_at):
started_at = timezone.make_aware(started_at)
if timezone.is_naive(ended_at):
ended_at = timezone.make_aware(ended_at)

if ended_at <= started_at:
msg = "End date must be after start date."
raise ValidationError(message=msg)

if started_at < program_started_at:
msg = "Module start date cannot be before program start date."
raise ValidationError(message=msg)

if ended_at > program_ended_at:
msg = "Module end date cannot be after program end date."
raise ValidationError(message=msg)

return started_at, ended_at


@strawberry.type
class ModuleMutation:
"""GraphQL mutations related to the mentorship Module model."""

@strawberry.mutation(permission_classes=[IsAuthenticated])
@transaction.atomic
def create_module(self, info: strawberry.Info, input_data: CreateModuleInput) -> ModuleNode:
"""Create a new mentorship module. User must be an admin of the program."""
user = info.context.request.user

try:
program = Program.objects.get(key=input_data.program_key)
project = Project.objects.get(id=input_data.project_id)
creator_as_mentor = Mentor.objects.get(nest_user=user)
except (Program.DoesNotExist, Project.DoesNotExist) as e:
msg = f"{e.__class__.__name__} matching query does not exist."
raise ObjectDoesNotExist(msg) from e
except Mentor.DoesNotExist as e:
msg = "Only mentors can create modules."
raise PermissionDenied(msg) from e

if not program.admins.filter(id=creator_as_mentor.id).exists():
raise PermissionDenied

started_at, ended_at = _validate_module_dates(
input_data.started_at,
input_data.ended_at,
program.started_at,
program.ended_at,
)

module = Module.objects.create(
name=input_data.name,
description=input_data.description,
experience_level=input_data.experience_level.value,
started_at=started_at,
ended_at=ended_at,
domains=input_data.domains,
tags=input_data.tags,
program=program,
project=project,
)

if module.experience_level not in program.experience_levels:
program.experience_levels.append(module.experience_level)
program.save(update_fields=["experience_levels"])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The program modification must invalidate Algolia cache we have on the backend.


mentors_to_set = resolve_mentors_from_logins(input_data.mentor_logins or [])
mentors_to_set.add(creator_as_mentor)
module.mentors.set(list(mentors_to_set))

return module

@strawberry.mutation(permission_classes=[IsAuthenticated])
@transaction.atomic
def update_module(self, info: strawberry.Info, input_data: UpdateModuleInput) -> ModuleNode:
"""Update an existing mentorship module. User must be an admin of the program."""
user = info.context.request.user

try:
module = Module.objects.select_related("program").get(
key=input_data.key, program__key=input_data.program_key
)
except Module.DoesNotExist as e:
msg = "Module not found."
raise ObjectDoesNotExist(msg) from e

try:
creator_as_mentor = Mentor.objects.get(nest_user=user)
except Mentor.DoesNotExist as err:
msg = "Only mentors can edit modules."
logger.warning(
"User '%s' is not a mentor and cannot edit modules.",
user.username,
exc_info=True,
)
raise PermissionDenied(msg) from err

if not module.program.admins.filter(id=creator_as_mentor.id).exists():
raise PermissionDenied

started_at, ended_at = _validate_module_dates(
input_data.started_at,
input_data.ended_at,
module.program.started_at,
module.program.ended_at,
)

old_experience_level = module.experience_level

field_mapping = {
"name": input_data.name,
"description": input_data.description,
"experience_level": input_data.experience_level.value,
"started_at": started_at,
"ended_at": ended_at,
"domains": input_data.domains,
"tags": input_data.tags,
}

for field, value in field_mapping.items():
setattr(module, field, value)

try:
module.project = Project.objects.get(id=input_data.project_id)
except Project.DoesNotExist as err:
msg = f"Project with id '{input_data.project_id}' not found."
logger.warning(msg, exc_info=True)
raise ObjectDoesNotExist(msg) from err

if input_data.mentor_logins is not None:
mentors_to_set = resolve_mentors_from_logins(input_data.mentor_logins)
module.mentors.set(mentors_to_set)

module.save()

if module.experience_level not in module.program.experience_levels:
module.program.experience_levels.append(module.experience_level)

# Remove old experience level if no other module is using it
if (
old_experience_level != module.experience_level
and old_experience_level in module.program.experience_levels
and not Module.objects.filter(
program=module.program, experience_level=old_experience_level
)
.exclude(id=module.id)
.exists()
):
module.program.experience_levels.remove(old_experience_level)

module.program.save(update_fields=["experience_levels"])

return module
Loading