diff --git a/backend/projects/admin.py b/backend/projects/admin.py index a377dfe..e4cdf81 100644 --- a/backend/projects/admin.py +++ b/backend/projects/admin.py @@ -4,7 +4,7 @@ # Register the Project model @admin.register(Project) class ProjectAdmin(admin.ModelAdmin): - list_display = ('title', 'group', 'institution', 'match_percentage', 'followers_count', 'likes_count') + list_display = ('title', 'group', 'institution', 'followers_count', 'likes_count') search_fields = ('title', 'group', 'institution') list_filter = ('group', 'institution') diff --git a/backend/projects/migrations/0003_remove_project_match_percentage.py b/backend/projects/migrations/0003_remove_project_match_percentage.py new file mode 100644 index 0000000..8a01ca3 --- /dev/null +++ b/backend/projects/migrations/0003_remove_project_match_percentage.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.7 on 2025-04-26 03:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0002_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='project', + name='match_percentage', + ), + ] diff --git a/backend/projects/models.py b/backend/projects/models.py index 681bb20..25bf6c8 100644 --- a/backend/projects/models.py +++ b/backend/projects/models.py @@ -18,7 +18,6 @@ class Project(models.Model): group = models.CharField(max_length=255, blank=True, null=True) followers_count = models.IntegerField(default=0) likes_count = models.IntegerField(default=0) - match_percentage = models.FloatField(blank=True, null=True) location = models.JSONField(default=list, blank=True) images = models.JSONField(default=list, blank=True) interest_tags = ArrayField(models.CharField(max_length=225), blank=True, null=True) @@ -35,7 +34,6 @@ class Project(models.Model): blank=True ) - # todo: see if we can remove this without hurting anything owner = models.ForeignKey( 'userauth.User', on_delete=models.CASCADE, diff --git a/backend/projects/tests.py b/backend/projects/tests.py index 354f0a7..0178a92 100644 --- a/backend/projects/tests.py +++ b/backend/projects/tests.py @@ -11,7 +11,6 @@ def setUp(self): # Create a sample project record self.project = Project.objects.create( group="CPP SEA", - match_percentage=85.5, title="Icebreak", institution="Cal Poly Pomona", description="We help connect organizations with their members.", @@ -23,7 +22,6 @@ def setUp(self): # Rebecca's test project self.project2 = Project.objects.create( group="Codebreaker's Club", - match_percentage=20, title="Enigma Machine Emulator", institution="University of California, Irvine", description="An Enigma machine emulator that replicates the encryption process of the Enigma M3 series used by the Germans.", @@ -35,7 +33,6 @@ def setUp(self): # Luis' test project self.project3 = Project.objects.create( group="CodeWarriors", - match_percentage=60, title="PersonalityPlaylist", institution="University of California, Riverside", description="An app to create custom Spotify playlists based on a personality profile.", @@ -47,7 +44,6 @@ def setUp(self): #William's test project self.project4 = Project.objects.create( group="Byte Me", - match_percentage=10, title="F1 Race Machine Learning Model", institution="University of California, Berkley", description="A machine learning model that takes data from F1 races and predicts winners based on previous results.", @@ -59,7 +55,6 @@ def setUp(self): def test_project_creation_1(self): # Check if the record populated correctly self.assertEqual(self.project.group, "CPP SEA") - self.assertEqual(self.project.match_percentage, 85.5) self.assertEqual(self.project.title, "Icebreak") self.assertEqual(self.project.institution, "Cal Poly Pomona") self.assertEqual(self.project.description, "We help connect organizations with their members.") @@ -71,7 +66,6 @@ def test_project_creation_1(self): # Rebecca's test project def test_project_creation_2(self): self.assertEqual(self.project2.group, "Codebreaker's Club") - self.assertEqual(self.project2.match_percentage, 20) self.assertEqual(self.project2.title, "Enigma Machine Emulator") self.assertEqual(self.project2.institution, "University of California, Irvine") self.assertEqual(self.project2.description, "An Enigma machine emulator that replicates the encryption process of the Enigma M3 series used by the Germans.") @@ -84,7 +78,6 @@ def test_project_creation_2(self): def test_project_creation_3(self): # Check if the record populated correctly self.assertEqual(self.project3.group, "CodeWarriors") - self.assertEqual(self.project3.match_percentage, 60) self.assertEqual(self.project3.title, "PersonalityPlaylist") self.assertEqual(self.project3.institution, "University of California, Riverside") self.assertEqual(self.project3.description, "An app to create custom Spotify playlists based on a personality profile.") @@ -97,7 +90,6 @@ def test_project_creation_3(self): def test_project_creation_4(self): # Check if the record populated correctly self.assertEqual(self.project4.group, "Byte Me") - self.assertEqual(self.project4.match_percentage, 10) self.assertEqual(self.project4.title, "F1 Race Machine Learning Model") self.assertEqual(self.project4.institution, "University of California, Berkley") self.assertEqual(self.project4.description, "A machine learning model that takes data from F1 races and predicts winners based on previous results.") @@ -110,10 +102,10 @@ class GetProjectsAPITest(APITestCase): def setUp(self): # Add mock project data self.project1 = Project.objects.create( - title="AI for Healthcare", institution="MIT", match_percentage=85.5 + title="AI for Healthcare", institution="MIT" ) self.project2 = Project.objects.create( - title="Web Dev", institution="Stanford", match_percentage=70.0 + title="Web Dev", institution="Stanford" ) self.url = reverse("get_projects") diff --git a/backend/projects/urls.py b/backend/projects/urls.py index 3f27e09..d441ef4 100644 --- a/backend/projects/urls.py +++ b/backend/projects/urls.py @@ -1,10 +1,11 @@ from django.urls import path -from .views import ProjectCRUDView, get_projects, toggle_follow, toggle_like +from .views import ProjectCRUDView, get_projects, toggle_follow, toggle_like, project_member_hander urlpatterns = [ path('', get_projects, name='get_projects'), # Fetch all projects path('create/', ProjectCRUDView.as_view(), name='project-create'), # Create project path('/', ProjectCRUDView.as_view(), name='project-detail'), # Read, Update, Delete project by ID path('like/', toggle_like, name='toggle_like'), - path('follow/', toggle_follow, name='toggle_follow') + path('follow/', toggle_follow, name='toggle_follow'), + path('member/manage/', project_member_hander, name='project-member-handler'), ] diff --git a/backend/projects/views.py b/backend/projects/views.py index b21bb57..bbb9224 100644 --- a/backend/projects/views.py +++ b/backend/projects/views.py @@ -122,3 +122,36 @@ def toggle_follow(request, project_id): return Response({'message': 'Project unfollowed successfully'}, status=status.HTTP_200_OK) else: return Response({'message': 'You already unfollowed this project.'}, status=status.HTTP_400_BAD_REQUEST) + +@api_view(['PATCH']) +@permission_classes([AllowAny]) +def project_member_hander(request, project_id): + try: + project = Project.objects.get(id=project_id) + user = User.objects.get(id=request.data['user_id']) + action = request.data.get('action') + + is_member = project.members.filter(id=user.id).exists() + + if action == "join": + if is_member: + return Response({'error': 'User is already a member of this project'}, status=400) + project.members.add(user) + user.projects.add(project) + return Response({'message': 'Member added successfully'}) + + elif action == "leave": + if not is_member: + return Response({'error': 'User is not a member of this project'}, status=400) + project.members.remove(user) + user.projects.remove(project) + return Response({'message': 'Member removed successfully'}) + + else: + return Response({'error': 'Invalid action'}, status=400) + + except Project.DoesNotExist: + return Response({'error': 'Project not found'}, status=404) + except User.DoesNotExist: + return Response({'error': 'User not found'}, status=404) + diff --git a/backend/userauth/admin.py b/backend/userauth/admin.py index a4d6528..bde25d4 100644 --- a/backend/userauth/admin.py +++ b/backend/userauth/admin.py @@ -10,4 +10,4 @@ class UserAdmin(admin.ModelAdmin): ) search_fields = ('username', 'email', 'first_name', 'last_name') list_filter = ('location',) - filter_horizontal = ('projects', 'owned') + filter_horizontal = ('projects',) diff --git a/backend/userauth/migrations/0010_remove_user_owned.py b/backend/userauth/migrations/0010_remove_user_owned.py new file mode 100644 index 0000000..dc9708a --- /dev/null +++ b/backend/userauth/migrations/0010_remove_user_owned.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.7 on 2025-04-26 03:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('userauth', '0009_remove_user_colleges_user_about_me_user_college_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='owned', + ), + ] diff --git a/backend/userauth/models.py b/backend/userauth/models.py index 64d7892..23c384b 100644 --- a/backend/userauth/models.py +++ b/backend/userauth/models.py @@ -26,11 +26,6 @@ class User(models.Model): related_name='contributors', blank=True ) - owned = models.ManyToManyField( - 'projects.Project', - related_name='owners', - blank=True - ) def __str__(self): return self.username or f"User-{self.id}" diff --git a/frontend/bitmatch/src/views/IndividualProjectPage.jsx b/frontend/bitmatch/src/views/IndividualProjectPage.jsx index a580b59..2f33f38 100644 --- a/frontend/bitmatch/src/views/IndividualProjectPage.jsx +++ b/frontend/bitmatch/src/views/IndividualProjectPage.jsx @@ -4,6 +4,8 @@ import { Plus, ThumbsUp, UserRound, + CirclePlus, + LogOut, Star, } from "lucide-react"; import { AnimatePresence, motion } from "framer-motion"; @@ -63,6 +65,8 @@ const ProjectDetailPage = () => { const [likeStatus, setLiked] = useState(false); const [userUuid, setUserUuid] = useState(null); const [cooldown, setCooldown] = useState(false); + const [isMember, setIsMember] = useState(false); + const [isReady, setIsReady] = useState(false); useEffect(() => { const fetchUserUuid = async () => { @@ -102,7 +106,14 @@ const ProjectDetailPage = () => { }; loadProjectInfo(); - }, [id]); + }, [id, userUuid]); + + useEffect(() => { + if (userData && project) { + setIsMember(project.members.includes(userData.id)); + setIsReady(true); + } + }, [userData, project]); const handleFollow = async () => { setFollowing(!following); @@ -125,6 +136,62 @@ const ProjectDetailPage = () => { } }; + const handleJoin = async () => { + const confirmed = window.confirm( + "Are you sure you want to join this project?" + ); + if (!confirmed) return; + try { + const response = await fetch( + `${SERVER_HOST}/projects/member/manage/${id}`, + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user_id: userData.id, + action: "join", + }), + } + ); + + if (response.ok) { + setIsMember(true); + } + } catch (error) { + console.error("Error joining project:", error); + } + }; + + const handleLeave = async () => { + const confirmed = window.confirm( + "Are you sure you want to leave this project?" + ); + if (!confirmed) return; + try { + const response = await fetch( + `${SERVER_HOST}/projects/member/manage/${id}`, + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user_id: userData.id, + action: "leave", + }), + } + ); + + if (response.ok) { + setIsMember(false); + } + } catch (error) { + console.error("Error leaving project:", error); + } + }; + const ai_feedback = async () => { if (cooldown) return; setCooldown(true); @@ -463,6 +530,38 @@ const ProjectDetailPage = () => { {project.followers_count} Followers + +
+ {isReady && userUuid !== project.owner && ( + <> + + + {isMember ? "Leave" : "Join"} + + + )} +