-
-
Notifications
You must be signed in to change notification settings - Fork 254
Created Module and program pages #1717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 e9ac12d
UI for program create , view and edit
Rajgupta36 b58802f
backend completed
Rajgupta36 abf873b
create module edit and view page
Rajgupta36 79b6268
type fix
Rajgupta36 eb8f076
update components
Rajgupta36 d6231c0
updated backend
Rajgupta36 fbb82ea
slugify urls
Rajgupta36 8d571fc
pre commit
Rajgupta36 468f0b8
fix components
Rajgupta36 a3c5d3b
fix merge conflicts
Rajgupta36 dbed9b9
update suggestions
Rajgupta36 be106ca
Merge branch 'main' into feat/mentorship-dev
Rajgupta36 ca3d122
update pnpm-lock file
Rajgupta36 ad1d76f
fix spell check
Rajgupta36 779df27
fix publish program
Rajgupta36 fb951f6
fix unit test cases
Rajgupta36 93236e4
Merge branch 'main' into feat/mentorship-dev
kasya c285a54
fix pre-commits
Rajgupta36 d2f0864
update frontend logic
Rajgupta36 a98cd10
format
Rajgupta36 471d502
refactor code
Rajgupta36 d0e8c7a
fix terminal
Rajgupta36 c96ed0b
suggestion fixes
Rajgupta36 3c1a25a
added unit test cases
Rajgupta36 99474f3
fix test cases
Rajgupta36 028e6b9
optimization
Rajgupta36 53035d1
Merge branch 'main' into feat/mentorship-dev
Rajgupta36 668b0ea
generated code
Rajgupta36 8844c2d
update code
Rajgupta36 fe3ff9e
update code
Rajgupta36 0b09822
update naming,validation logic
Rajgupta36 454cd66
Merge branch 'main' into feat/mentorship-dev
Rajgupta36 94cceed
cleanup
Rajgupta36 4d56bcf
fix project suggestion bug
Rajgupta36 1579788
Merge branch 'main' into feat/mentorship-dev
Rajgupta36 0468fcf
update code
Rajgupta36 d5c4c91
merge main into feat/mentorship-dev
Rajgupta36 3a1ee00
fixes
Rajgupta36 8d5e9fd
apply suggestions
Rajgupta36 3cbb44c
merge main into feat/mentorship-dev
Rajgupta36 32962c9
update suggestion
Rajgupta36 d1d89ab
merge main into feat/mentorship-dev
Rajgupta36 a67a3fb
update code
Rajgupta36 aa42c75
merge main into feat/mentorship-dev
Rajgupta36 45fe1e0
merge main into feat/mentorship-dev
Rajgupta36 7596754
Merge branch 'main' into feat/mentorship-dev
kasya d806214
merge main into feat/mentorship-dev
Rajgupta36 8ba10b8
update code
Rajgupta36 61b3725
Merge branch 'main' into feat/mentorship-dev
Rajgupta36 30ff8e7
update frozen-lockfile
Rajgupta36 b518fc9
Merge branch 'main' into feat/mentorship-dev
kasya 8eb6522
merge main into feat/mentorship-dev
Rajgupta36 81b48aa
Merge remote-tracking branch 'upstream/main' into feat/mentorship-dev
Rajgupta36 478d3cf
update code and make program page view only
Rajgupta36 4ddf608
updated migration and add query
Rajgupta36 8b4e085
added index
Rajgupta36 cb8665a
updated components, mutaitons and nodes
Rajgupta36 2e8fc2a
update code
Rajgupta36 7197275
added constraints
Rajgupta36 63ecb63
fix test case
Rajgupta36 eb24e0b
merge main into feat/mentorship-dev
Rajgupta36 4b7d5dc
added unit test case for program page
Rajgupta36 8e7cb8f
merge main into feat/mentorship-dev
Rajgupta36 b3f51ec
added remaining unit test cases
Rajgupta36 5689e93
fix bug
Rajgupta36 b98893f
merge main into feat/mentorship-dev
Rajgupta36 f1d3b24
update code
Rajgupta36 27839f8
merge main into feat/mentorship-dev
Rajgupta36 a27a4d6
updated auth and projectleader code
Rajgupta36 ee84b4a
update test cases
Rajgupta36 d2ef0ce
update code
Rajgupta36 06bb530
revert test case
Rajgupta36 80a83c0
merge upstream into main
Rajgupta36 29afecd
fix unit test cases
Rajgupta36 04e060b
migration fix
Rajgupta36 0405292
merge upstream into main
Rajgupta36 f446ecf
increase test coverage
Rajgupta36 3e4c9e4
update suggestions
Rajgupta36 75ae1ba
added function for cache clear by index_name
Rajgupta36 865dbac
created component for single module
Rajgupta36 9e28465
fix typo
Rajgupta36 a9a734c
fix some frontend test cases
Rajgupta36 2f5e88d
added post save signal
Rajgupta36 3e51bec
added more test cases
Rajgupta36 1b6f6f4
merge upstream into main
Rajgupta36 95f824b
merge upstream into main and update singlemodulecard
Rajgupta36 7fe07d4
pnpm updated
Rajgupta36 e3b1c5b
increase coverage
Rajgupta36 14f5519
Merge branch 'main' into feat/mentorship-dev
kasya df539bc
update suggestion
Rajgupta36 9674fb0
updated backend suggestions
Rajgupta36 c522b1c
merge upstream into main
Rajgupta36 f66ddde
added isMentor query
Rajgupta36 376ccdb
fix page loading
Rajgupta36 dc97d9a
merge upstream into main
Rajgupta36 dd6f087
Update module description styling
kasya fb1f7e8
merge upstream into main
Rajgupta36 e4fa7a4
merge upstream into main
Rajgupta36 87fbdd2
Update code
arkid15r File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| """Core app constants.""" | ||
|
|
||
| CACHE_PREFIX = "algolia_proxy" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
197
backend/apps/mentorship/api/internal/mutations/module.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"]) | ||
|
|
||
| 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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.