Skip to content
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

implemented backend logic for retrieving and updating a user profile #481

Closed
wants to merge 7 commits into from
Closed
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
dynamic user routing works now I think
ElaineShu0312 committed Aug 8, 2024
commit 5085cc87c0b789ae67685791bd2761eb6d653ca2
12 changes: 10 additions & 2 deletions csm_web/frontend/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import { emptyRoles, Roles } from "../utils/user";
import CourseMenu from "./CourseMenu";
import Home from "./Home";
import Policies from "./Policies";
import UserProfile from "./UserProfile";
import { DataExport } from "./data_export/DataExport";
import { EnrollmentMatcher } from "./enrollment_automation/EnrollmentMatcher";
import { Resources } from "./resource_aggregation/Resources";
@@ -42,8 +43,12 @@ const App = () => {
<Route path="policies/*" element={<Policies />} />
<Route path="export/*" element={<DataExport />} />
{
// TODO: add route for profiles (/user/:id/* element = {UserProfile})
// TODO: add route for profiles (/profile/:id/* element = {UserProfile})
// TODO: add route for your own profile /profile/*
// reference Section
}
<Route path="profile/*" element={<UserProfile />} />
<Route path="profile/:id/*" element={<UserProfile />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
@@ -82,7 +87,7 @@ function Header(): React.ReactElement {
};

/**
* Helper function to determine class name for the home NavLInk component;
* Helper function to determine class name for the home NavLnk component;
* is always active unless we're in another tab.
*/
const homeNavlinkClass = () => {
@@ -143,6 +148,9 @@ function Header(): React.ReactElement {
<NavLink to="/policies" className={navlinkClassSubtitle}>
<h3 className="site-subtitle">Policies</h3>
</NavLink>
<NavLink to="/profile" className={navlinkClassSubtitle}>
<h3 className="site-subtitle">Profile</h3>
</NavLink>
<a id="logout-btn" href="/logout" title="Log out">
<LogOutIcon className="icon" />
</a>
2 changes: 1 addition & 1 deletion csm_web/frontend/src/components/CourseMenu.tsx
Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@ import React, { useEffect, useState } from "react";
import { Link, Route, Routes } from "react-router-dom";

import { DEFAULT_LONG_LOCALE_OPTIONS, DEFAULT_TIMEZONE } from "../utils/datetime";
import { useUserInfo } from "../utils/queries/base";
import { useCourses } from "../utils/queries/courses";
import { useUserInfo } from "../utils/queries/user";
import { Course as CourseType, UserInfo } from "../utils/types";
import LoadingSpinner from "./LoadingSpinner";
import Course from "./course/Course";
70 changes: 70 additions & 0 deletions csm_web/frontend/src/components/UserProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from "react";
import { useParams } from "react-router-dom";
import { useUser } from "../utils/queries/user";
import LoadingSpinner from "./LoadingSpinner";
import "../css/base/form.scss"; // Import the base.scss file for styling
import "../css/base/table.scss";

const UserProfile: React.FC = () => {
const { id } = useParams(); // Type the id parameter
const { data, error, isLoading } = useUser(Number(id));

// Handle loading and error states
if (isLoading) {
return <LoadingSpinner className="spinner-centered" />;
}
if (error) {
return <div>Error: {error.message}</div>;
}

return (
<div className="csm-form">
<h2>User Profile</h2>
<form>
<div className="form-label">
<label htmlFor="firstName">First Name:</label>
<p id="firstName" className="form-input">
{data?.firstName}
</p>
</div>
<div className="form-label">
<label htmlFor="lastName">Last Name:</label>
<p id="lastName" className="form-input">
{data?.lastName}
</p>
</div>
<div className="form-label">
<label htmlFor="email">Email:</label>
<p id="email" className="form-input">
{data?.email}
</p>
</div>
<div className="form-label">
<label htmlFor="bio">Bio:</label>
<p id="bio" className="form-input">
{data?.bio || "N/A"}
</p>
</div>
<div className="form-label">
<label htmlFor="pronouns">Pronouns:</label>
<p id="pronouns" className="form-input">
{data?.pronouns || "N/A"}
</p>
</div>
<div className="form-label">
<label htmlFor="pronunciation">Pronunciation:</label>
<p id="pronunciation" className="form-input">
{data?.pronunciation || "N/A"}
</p>
</div>
<div className="form-actions">
<button type="button" className="form-select" onClick={() => console.log("Edit profile")}>
Edit
</button>
</div>
</form>
</div>
);
};

export default UserProfile;
1 change: 0 additions & 1 deletion csm_web/frontend/src/components/section/Section.tsx
Original file line number Diff line number Diff line change
@@ -13,7 +13,6 @@ import "../../css/section.scss";

export default function Section(): React.ReactElement | null {
const { id } = useParams();

const { data: section, isSuccess: sectionLoaded, isError: sectionLoadError } = useSection(Number(id));

if (!sectionLoaded) {
48 changes: 1 addition & 47 deletions csm_web/frontend/src/utils/queries/base.tsx
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@

import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { fetchNormalized } from "../api";
import { Profile, RawUserInfo } from "../types";
import { Profile } from "../types";
import { handleError, handlePermissionsError, handleRetry, ServerError } from "./helpers";

/**
@@ -34,49 +34,3 @@ export const useProfiles = (): UseQueryResult<Profile[], Error> => {
handleError(queryResult);
return queryResult;
};

// TODO: move to new queries/users.tsx
/**
* Hook to get the user's info.
*/
export const useUserInfo = (): UseQueryResult<RawUserInfo, Error> => {
const queryResult = useQuery<RawUserInfo, Error>(
["user"],
async () => {
const response = await fetchNormalized("/user");
if (response.ok) {
return await response.json();
} else {
handlePermissionsError(response.status);
throw new ServerError("Failed to fetch user info");
}
},
{ retry: handleRetry }
);

handleError(queryResult);
return queryResult;
};

// TODO: move to new queries/users.tsx
/**
* Hook to get a list of all user emails.
*/
export const useUserEmails = (): UseQueryResult<string[], Error> => {
const queryResult = useQuery<string[], Error>(
["users"],
async () => {
const response = await fetchNormalized("/users");
if (response.ok) {
return await response.json();
} else {
handlePermissionsError(response.status);
throw new ServerError("Failed to fetch user info");
}
},
{ retry: handleRetry }
);

handleError(queryResult);
return queryResult;
};
103 changes: 103 additions & 0 deletions csm_web/frontend/src/utils/queries/user.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { fetchNormalized } from "../api";
import { RawUserInfo } from "../types";
import { handleError, handlePermissionsError, handleRetry, ServerError } from "./helpers";

/**
* Hook to get a list of all user emails.
*/
export const useUserEmails = (): UseQueryResult<string[], Error> => {
const queryResult = useQuery<string[], Error>(
["users"],
async () => {
const response = await fetchNormalized("/users");
if (response.ok) {
return await response.json();
} else {
handlePermissionsError(response.status);
throw new ServerError("Failed to fetch user info");
}
},
{ retry: handleRetry }
);

handleError(queryResult);
return queryResult;
};

/**
* Hook to get the current user's info.
*/
// TODO: merge with useUserDetails
export const useUserInfo = (): UseQueryResult<RawUserInfo, Error> => {
const queryResult = useQuery<RawUserInfo, Error>(
["user"],
async () => {
const response = await fetchNormalized("/user");
if (response.ok) {
return await response.json();
} else {
handlePermissionsError(response.status);
throw new ServerError("Failed to fetch user info");
}
},
{ retry: handleRetry }
);

handleError(queryResult);
return queryResult;
};

/**
* Hook to get the requested user's info
*/
// TODO: handle if there is no userId
export const useUserDetails = (userId?: number): UseQueryResult<RawUserInfo, Error> => {
const queryResult = useQuery<RawUserInfo, Error>(
["userDetails", userId],
async () => {
const response = await fetchNormalized(`/user/${userId}`);
if (response.ok) {
return await response.json();
} else {
handlePermissionsError(response.status);
throw new ServerError("Failed to fetch user details");
}
},
{
retry: handleRetry,
enabled: !!userId // only run query if userId is available
}
);

handleError(queryResult);
return queryResult;
};

/**
* Hook to get user info. If userId is provided, fetches details for that user; otherwise, fetches current user's info.
*/
export const useUser = (userId?: number): UseQueryResult<RawUserInfo, Error> => {
const queryKey = userId ? ["userDetails", userId] : ["user"];

const queryResult = useQuery<RawUserInfo, Error>(
queryKey,
async () => {
const endpoint = userId ? `/user/${userId}` : "/user";
const response = await fetchNormalized(endpoint);
if (response.ok) {
return await response.json();
} else {
handlePermissionsError(response.status);
throw new ServerError(userId ? "Failed to fetch user details" : "Failed to fetch user info");
}
},
{
retry: handleRetry,
enabled: userId !== undefined // Only run the query if userId is provided or if fetching current user's info
}
);

handleError(queryResult);
return queryResult;
};
3 changes: 3 additions & 0 deletions csm_web/frontend/src/utils/types.tsx
Original file line number Diff line number Diff line change
@@ -36,6 +36,9 @@ export interface UserInfo {
lastName: string;
email: string;
priorityEnrollment?: DateTime;
bio: string;
pronouns: string;
pronunciation: string;
}

/**
5 changes: 5 additions & 0 deletions csm_web/scheduler/tests/models/test_user.py
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
import pytest
from django.urls import reverse
from scheduler.factories import (
CoordinatorFactory,
CourseFactory,
MentorFactory,
SectionFactory,
@@ -46,6 +47,7 @@ def fixture_setup_permissions():
# Assign mentors to courses
mentor_a = MentorFactory(user=mentor_user, course=course_a)
mentor_b = MentorFactory(user=other_mentor_user, course=course_b)
coordinator_a = CoordinatorFactory(user=coordinator_user, course=course_a)

# Create sections associated with the correct course via the mentor
section_a1 = SectionFactory(mentor=mentor_a)
@@ -63,6 +65,7 @@ def fixture_setup_permissions():
"mentor_user": mentor_user,
"other_mentor_user": other_mentor_user,
"coordinator_user": coordinator_user,
"coordinator_a": coordinator_a,
"course_a": course_a,
"course_b": course_b,
"section_a1": section_a1,
@@ -137,6 +140,8 @@ def test_student_edit_own_profile(client, setup_permissions):
##############
# Mentor tests
##############


@pytest.mark.django_db
def test_mentor_view_own_profile(client, setup_permissions):
"""