Django + React 電商專案練習 [???] - 後台實作-管理、刪除、更新用戶資料

Posted by Young on 2022-12-29
Estimated Reading Time 19 Minutes
Words 4.2k In Total

取得 Admin Token

由於在 user_views.py 中,使用 IsAdminUser 來限制只有 Admin 才能取得 User 資料,所以需要先登入 Admin 帳號,取得 Admin 使用者 Token,才能透過 Postman 取得 User 資料。

1
2
3
4
5
6
@api_view(['GET']) # 取所有 User
@permission_classes([IsAdminUser]) # 只有Admin才能 access
def getUsers(request):
users = User.objects.all()
serializer = UserSerializer(users,many=True)
return Response(serializer.data)

若套用一般使用者的 Token 會出現以下錯誤:

no_permission

因此先登入 Admin 帳號,取得 Admin 使用者 Token,才能透過 Postman 取得所有 User 的資料。

admin_user_token

用戶列表前端實作

Constants 定義

frontend/src/constants/userConstants.js 定義以下常數:

1
2
3
4
5
....
export const USER_LIST_REQUEST = "USER_LIST_REQUEST";
export const USER_LIST_SUCCESS = "USER_LIST_SUCCESS";
export const USER_LIST_FAIL = "USER_LIST_FAIL";
export const USER_LIST_RESET = "USER_LIST_RESET";

Reducers 實作

這邊 state 會需要多一個 users 陣列來存放所有 User 資料,然後在 success 終究不在需要 success: true了,因為這邊只是取得 User 資料,不需要判斷是否成功更新。然後 userInfo 改成 user,一樣都是儲存 action 傳遞過來的 payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const userListReducer = (state = { users: [] }, action) => {
switch (action.type) {
case USER_LIST_REQUEST:
return { loading: true };
case USER_LIST_SUCCESS:
return { loading: false, user: action.payload }; // success 是為了在 ProfilePage.js output 判斷是否成功更新
case USER_LIST_FAIL:
return { loading: false, error: action.payload };
case USER_LIST_RESET: // 這邊是為了在 ProfilePage.js 中的 useEffect 中,當 user 更新成功後,清空 state
return { users: [] };
default:
return state;
}
};

一樣到 store.js 中加入 userListReducer

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ...,userListReducer } from "./reducers/userReducers";

const store = configureStore({
reducer: {
// 儲存多個state的地方,到時會再各個component用useSelector指定要使用哪一個

// 用戶相關
...
userList: userListReducer,
},
preloadedState: initialState,
middleware: middleware,
});

此時去網頁開啟 Redux DevTools,應能看到多了一個 userList 的 state:

redux_tool

Actions 實作

這邊直接複製 updateUserProfile 的 actio 來修改,因為都是取得 User 資料,只是 endpoint 不同而已。

  • 將 put 改成 get
  • /api/users/profile/ 改成 /api/users/
  • 拿掉 localStorage 的更新,因為這邊只是取得 User 資料,不需要更新 localStorage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// userActions.js
// 更新使用者資料
export const listUsers = (user) => async (dispatch, getState) => {
// getState 大多都是在需要訪問 store 的裡的數據以做一些操作時使用
try {
dispatch({
type: USER_LIST_REQUEST,
});

const {
userLogin: { userInfo },
} = getState(); // 確保 User 是登入的狀態

const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userInfo.token}`, // 需要有 token 才能取得資料,因為後端有設 permission_classes
},
};
const { data } = await axios.get(
`/api/users/`,
user, // 這邊的 user 是從 ProfilePage.js 傳過來的
config
);

dispatch({
type: USER_LIST_SUCCESS,
payload: data, // payload 是後端回傳的資料
});
} catch (error) {
dispatch({
type: USER_LIST_FAIL,
payload: error.response && error.response.data.message ? error.response.data.message : error.message,
});
}
};

為了登出後清空 User 資料,所以在同檔案中的 logout 函式加上 USER_LIST_RESET

1
2
3
4
5
6
7
8
// userActions.js
export const logout = () => (dispatch) => {
localStorage.removeItem("userInfo");
dispatch({ type: USER_LOGOUT });
dispatch({ type: USER_DETAILS_RESET }); // 登出後要清空 user Object,否則會造成登出後還是會顯示上一個使用者的資料
dispatch({ type: ORDER_LIST_MY_RESET }); // 登出後清空訂單資料
dispatch({ type: USER_LIST_RESET }); // 登出後清空訂單資料
};

前端頁面實作

建立一個新的名為 UserListPage.js 的 component,這邊會使用到 useEffect 來取得 User 資料,並且使用 useDispatch 來 dispatch action 來取得 User 資料。

1
2
3
4
5
6
7
useEffect(() => {
dispatch(listUsers());
}, [dispatch]); // 當 dispatch有變動就觸發 useEffect
...
return(
...
)

回到 App.js 中註冊 UserListPage 的 Route,測試看頁面是否有成功顯示:

1
2
3
4
5
6
7
8
// App.js
import UserListPage from "./Pages/UserListPage";
...
<Routes>
<Route path="/" element={<HomePage/>} />
....
<Route path="/admin/userlist" element={<UserListPage/>} />
</Routes>4

非 Admin 使用者重新導向至登入頁面

判斷若非 Admin 使用者就直接不顯示這個下拉選項以及不能訪問該頁面 參考這篇 -> 管理員專屬後台頁面-路由結構最佳化寫法

製作顯示所有使用者資料的表格

user_table

在 Header 新增相應的下拉選項

這邊不直接使用 NavDropdown.Itemhref,而是使用 LinkContainer 來導向到 /admin/userlist,是因為要保有 React SPA 的優勢,不要重新整理頁面。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Header.js
userInfo && userInfo.isAdmin && (
<NavDropdown title='後台管理專用' id='admin-menu'>
<LinkContainer to='/admin/userlist'>
<NavDropdown.Item>用戶管理</NavDropdown.Item>
</LinkContainer>
<NavDropdown.Item>Another action</NavDropdown.Item>

<NavDropdown.Item>Something</NavDropdown.Item>
<NavDropdown.Divider />
<NavDropdown.Item>Separated link</NavDropdown.Item>
</NavDropdown>
);

刪除使用者資料實作

在前端簡單測試按鈕是否有成功觸發事件,並取得 User 的 id 後,就可以開始撰寫後端 API 來處理編輯、刪除使用者資料。

frontend_delete_user_test

1
2
3
4
5
6
7
const deleteUserHandler = (id) => {
console.log("delete", id);
};
...
<Button variant='danger' className='btn-sm' onClick={() => deleteUserHandler(user.id)}>
<i className='fas fa-trash'></i>
</Button>

後端 API 實作

一樣先從

  • user_views.py 中新增一個 deleteUser 的 API
  • urls.py 中新增一個 path('user/<str:pk>/', views.deleteUser, name='user-delete') 的路由

views.py

1
2
3
4
5
6
@api_view(['DELETE']) # 刪除 User
@permission_classes([IsAdminUser]) # 只有Admin才能 access
def deleteUser(request,pk):
userForDeletion = User.objects.get(id=pk)
userForDeletion.delete()
return Response('用戶已成功刪除')

urls.py

1
path('delete/<str:pk>/',views.deleteUser , name='user_delete'),

前端實作

Constants 定義

1
2
3
export const USER_DELETE_REQUEST = "USER_DELETE_REQUEST";
export const USER_DELETE_SUCCESS = "USER_DELETE_SUCCESS";
export const USER_DELETE_FAIL = "USER_DELETE_FAIL";

Reducers 實作

直接複製 userListReducer 函式來修改,這邊重點修改處:

  • state 初始值改為空物件,因為已不需要 users 陣列來存放 User 資料
  • USER_LIST_XXX 改為 USER_DELETE_XXX
  • 在 case USER_DELETE_SUCCESS 中,不再需要 users: action.payload
  • 去除 USER_LIST_RESET,因為這邊不需要清空 state
1
2
3
4
5
6
7
8
9
10
11
12
export const userDeleteReducer = (state = {}, action) => {
switch (action.type) {
case USER_DELETE_REQUEST:
return { loading: true };
case USER_DELETE_SUCCESS:
return { loading: false, success: true };
case USER_DELETE_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};

store.jsconfigureStore 中加入 userDeleteReducer

1
2
3
4
5
6
7
8
9
const store = configureStore({
reducer: {
// 用戶相關
...
userDelete: userDeleteReducer,
},
preloadedState: initialState,
middleware: middleware,
});

註冊完畢後,在前端開啟 Redux DevTools,應能看到多了一個 userDelete 的 state,此時裡面應該空物件的狀態。

user_delete_state

Actions 實作

一樣複製 listUsers 函式來修改,重點修改處:

  • 需要在 deleteUser 函式中傳入 id 來刪除指定 user
  • get 改為 delete
  • HTTP 請求路由改為 /api/users/delete/${id}/,這邊 ${id} 是從 UserListPage.js 傳過來的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 刪除使用者資料
export const deleteUser = (id) => async (dispatch, getState) => {
// getState 大多都是在需要訪問 store 的裡的數據以做一些操作時使用
try {
dispatch({
type: USER_DELETE_REQUEST,
});

const {
userLogin: { userInfo },
} = getState(); // 確保 User 是登入的狀態

const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userInfo.token}`, // 需要有 token 才能取得資料,因為後端有設 permission_classes
},
};
const { data } = await axios.delete(`/api/users/delete/${id}/`, config);

dispatch({
type: USER_DELETE_SUCCESS,
payload: data, // payload 是後端回傳的資料
});
} catch (error) {
dispatch({
type: USER_DELETE_FAIL,
payload: error.response && error.response.data.message ? error.response.data.message : error.message,
});
}
};

前端頁面中的按鈕實現刪除使用者資料功能

引入先前寫好的 deleteUser action,並從 Redux store 中讀取 userDelete 狀態,然後解構 successsuccessDelete(提取刪除操作是否成功的標誌,並重命名為 successDelete),最後再透過 useEffect 來監聽刪除結果,若成功刪除使用者後,自動重新載入頁面。

  • listUsers: 用來取得所有使用者的列表,並更新到 Redux store 中。
  • deleteUser: 用來發送刪除使用者的請求,並更新刪除的結果到 Redux store。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { listUsers,deleteUser } from "../actions/userActions";
...

const userDelete = useSelector((state) => state.userDelete);
const { success: successDelete } = userDelete;

useEffect(() => {
dispatch(listUsers());

}, [dispatch,successDelete]); // 這邊放入 successDelete,因為要在刪除使用者後自動重新載入頁面

const deleteUserHandler = (id) => {
// console.log("delete",id);
if(window.confirm("確定要刪除此使用者嗎?")){
dispatch(deleteUser(id));
}
}

這一段 useEffect 的白話文解析: 每次 successDelete 發生變化時(即刪除操作成功後),重新觸發 listUsers,從後端更新用戶列表。

觀念補充

至於為何不直接如以下這樣調用 userDelete.success,還需要解構 userDelete,再取出 success,再重新命名為 successDelete

1
2
3
4
5
const userDelete = useSelector((state) => state.userDelete);
// const { success: successDelete } = userDelete;
useEffect(() => {
dispatch(listUsers());
}, [dispatch, userDelete.success]);

是為了防止不必要的多次觸發,也是培養一個良好的寫程式習慣,一般在 useEffect 中,盡量不直接與 Redux store 中的整個 userDelete 物件進行比較,而是只比較 success 屬性,這樣可以避免不必要的重新渲染。

例如以上情況就是,只有當 userDelete.success 的值改變(例如從 false 變為 true)時,successDelete 才會更新,進而觸發 useEffect。

編輯使用者資料實作

先前只有做取得用戶個人資訊的功能,現在需要取得「所有用戶」個別的資訊,所以需先取得各個不同使用者的資料才能去進行編輯、更新,所以會需創建兩個後端的 Route,

後端 API 實作

views.py

與前面不同的是這邊的 user 都從 單單 request.user 改為 User.objects.get(id=pk),這樣才能取得特定 User 的資料,因為不再是取得登入用戶自己的資料,而是要取得指定 User 的資料。

1
2
3
4
5
6
@api_view(['GET']) # 後台用戶管理,取得單個用戶
@permission_classes([IsAdminUser]) # 只有Admin才能 access
def adminGetUserById(request,pk):
user = User.objects.get(id=pk) # 根據提供的 id 手動查詢特定用戶資料。
serializer = UserSerializer(user,many=False)
return Response(serializer.data)

更新特定 User 的資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@api_view(['PUT']) # 更新用戶的 profile
@permission_classes([IsAuthenticated]) # 只有登入的人才能 access
def adminUpdateUser(request,pk):
user = User.objects.get(id=pk)
serializer = UserSerializer(user,many=False)

data = request.data

user.first_name = data['first_name']
user.username = data['email']
user.email = data['email']
user.isAdmin = data['isAdmin']
# user.is_staff = data['isAdmin']

user.save()
return Response(serializer.data)

更新 urls.py

1
2
3
# Admin
path('<str:pk>/', views.adminGetUserById, name='user_get_by_id'),
path('update/<str:pk>/', views.adminUpdateUser, name='user_update'),

寫完後去 Postman 測試看看是否有成功取得特定 User 的資料,一樣應要代入 Admin Token 才能取得 User 資料。

postman_get_user

前端實作

編輯使用者頁面

建立一個名為 UserEditPage.js 的頁面,功能部分類似於註冊頁面

  • 透過 useParams 取得 URL 中用戶的 id,記得在App.js中若是定義:userID,那用 useParams 取時也一樣要用
    userID,而不是 id
1
2
3
// App.js
...
<Route path='/admin/user/:userID/edit' element={<AdminRoute><UserEditPage /></AdminRoute>} />
  • 透過 useDispatch 來 dispatch getUserDetails action 來取得特定 User 的資料
  • 透過 useSelector 來取得 userDetails 的狀態,並解構出 loadingerroruser 三個屬性
1
2
3
4
5
6
7
8
9
10
11
// UserEditPage.js
const UserEditPage = () => {
const { userID } = useParams(); // React v6以後要用這種方式來取得id
};
...
const userDetails = useSelector((state) => state.userDetails);
const { loading, error, user } = userDetails;

const submitHandler = (e) => {
e.preventDefault();
};

如果沒用 NumberuserID轉換成數字,user.id !== userID 就會始終為 true,dispatch(getUserDetails(userID)) 就會不停被觸發,Redux 狀態一直被重置為 loading: true,從而導致頁面不斷進入加載狀態。

這邊目的要能判斷當前用戶是否存在,並且用戶 id 是否等於當前用戶 id,如果是,就自動載入用戶資料,否則就發送取得用戶資料的請求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
useEffect(() => {
if (!user.first_name || user.id !== Number(userID)) {
dispatch(getUserDetails(userID)); // 如果用戶不存在或者用戶id不等於當前用戶id,就發送請求
} else {
// 如果用戶存在,就自動載入用戶資料
setFirst_name(user.first_name);
setEmail(user.email);
setIsAdmin(user.isAdmin);
}
}, [dispatch, user, userID]);

return (
<>
<Link to='/admin/userlist' className='btn btn-light my-3'>
回到用戶列表
</Link>

<h1>編輯用戶資料</h1>
{loading ? (
<Loader />
) : error ? (
<Message variant='danger'>{error}</Message>
) : (
<FormContainer>
<Form onSubmit={submitHandler}>
<Form.Group controlId='first_name' className='mb-3'>
<Form.Label>用戶姓名</Form.Label>
<Form.Control required type='name' placeholder='請輸入您的ID' value={first_name} onChange={(e) => setFirst_name(e.target.value)}></Form.Control>
</Form.Group>

<Form.Group controlId='email' className='mb-3'>
<Form.Label>電子郵件</Form.Label>
<Form.Control required type='email' placeholder='請輸入電子郵件' value={email} onChange={(e) => setEmail(e.target.value)} autoComplete='username'></Form.Control>
</Form.Group>

<Form.Group controlId='isAdmin'>
<Form.Check type='checkbox' label='是否為管理員' checked={isAdmin} onChange={(e) => setIsAdmin(e.target.checked)}></Form.Check>
</Form.Group>

<p>{isAdmin ? "User is an Admin" : "User is not an Admin"}</p>

<Button type='submit' variant='primary' className='mt-3'>
更新
</Button>
</Form>
</FormContainer>
)}
</>
);

解決從編輯用戶資料跳至個人資料頁面,資料顯示不正確的問題

目前若從用戶編輯頁面

edit_user_page

跳至用戶個人資料頁面,會發現資料載入不正確,必須要重新整理頁面才能正確載入。

profile_page

那這是由於在 useEffect 中,當 user.id !== Number(userID) 時,才會發送 getUserDetails(userID) 請求,但是當從編輯頁面跳至個人資料頁面時,user.id 並不會改變,所以就不會發送請求,這就是為什麼會出現資料不正確的問題。

解決方是就是回到個人資料頁面 ProfilePage.js 時,多檢查 userInfo.id 若不等於 user.id 就再發送一次getUserDetails(userInfo.id) 請求,重新獲取正確的用戶個人資料。

1
2
3
4
5
6
7
8
// ProfilePage.js
useEffect(() => {
...
if (!user || !user.first_name || success || userInfo.id !== user.id) {
...
}
...
}, [dispatch, userInfo, user, success]);

Constants 定義

1
2
3
4
// userConstants.js
export const USER_UPDATE_REQUEST = "USER_UPDATE_REQUEST";
export const USER_UPDATE_SUCCESS = "USER_UPDATE_SUCCESS";
export const USER_UPDATE_FAIL = "USER_UPDATE_FAIL";

Reducers 實作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// userReducers.js
export const userUpdateReducer = (state = { user: [] }, action) => {
switch (action.type) {
case USER_UPDATE_REQUEST:
return { loading: true };
case USER_UPDATE_SUCCESS:
return { loading: false, success: true };
case USER_UPDATE_FAIL:
return { loading: false, error: action.payload };
case USER_UPDATE_RESET:
return { user: [] }; // 重置 user 狀態,清空舊資料,避免影響下一次操作或導致錯誤的資料顯示
default:
return state;
}
};

然後一樣至 store.jsconfigureStore 中加入 userUpdateReducer,並回瀏覽器的 Redux DevTools 中檢查是否有 userUpdate 的 state。

Actions 實作

「更新操作的重點在於需要修改的資料,而不是指定哪個用戶」

  • updateUser 函式改傳入 user 而不是 id,因為更新需要多個欄位(例如名字、電子郵件、角色、狀態等),所以傳入整個 user 物件
  • axios 使用的是 put 方法,並帶入 user 物件,路由所帶入的 iduser.id
  • 不會在 UPDATE_SUCCESS 中帶入 payload:data,而是在更新成功用戶資料後,再次發送 USER_DETAILS_SUCCESS 來重新獲取用戶資料
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export const updateUser = (user) => async (dispatch, getState) => {
// getState 大多都是在需要訪問 store 的裡的數據以做一些操作時使用
try {
dispatch({
type: USER_UPDATE_REQUEST,
});

const {
userLogin: { userInfo },
} = getState(); // 確保 User 是登入的狀態

const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userInfo.token}`, // 需要有 token 才能取得資料,因為後端有設 permission_classes
},
};
const { data } = await axios.put(`/api/users/update/${user.id}/`, user, config);

dispatch({
type: USER_UPDATE_SUCCESS,
});

dispatch({
// 更新成功後自動重新獲取用戶列表
type: USER_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: USER_UPDATE_FAIL,
payload: error.response && error.response.data.message ? error.response.data.message : error.message,
});
}
};

前端頁面功能實作

回到用戶編輯頁面

  • import 在 userActions.js 中沒用到的 USER_UPDATE_RESET 常數,負責在更新成功後清空 user 狀態,避免影響下一次操作或導致錯誤的資料顯示
  • 更新完成後,自動導向到用戶列表頁面
  • 顯示更新操作的 loading 狀態,以及錯誤信息
  • 透過 useEffect 依賴陣列監聽 successUpdate 狀態,若更新成功,則清空 user 狀態,並自動導向到用戶列表頁面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// UserEditPage.js
import { ..., updateUser } from "../actions/userActions"; // 自動載入用戶資料
import { USER_UPDATE_RESET } from "../constants/userConstants";

const userUpdate = useSelector((state) => state.userUpdate);
// console.log("Redux state userDetails:", userDetails);
const { error: errorUpdate, loading: loadingUpdate, suecces: successUpdate } = userUpdate;

const submitHandler = (e) => {
e.preventDefault();
dispatch(updateUser({ id: userID, first_name, email, isAdmin }));
};

useEffect(() => {
if (successUpdate) {
dispatch({ type: USER_UPDATE_RESET });
navigate("/admin/userlist");
} else {
if (!user.first_name || user.id !== Number(userID)) {
dispatch(getUserDetails(userID)); // 如果用戶不存在或者用戶id不等於當前用戶id,就發送請求
} else {
// 如果用戶存在,就自動載入用戶資料
setFirst_name(user.first_name);
setEmail(user.email);
setIsAdmin(user.isAdmin);
}
}
}, [dispatch, user, userID, successUpdate, navigate]);

return(
...
<h1>編輯用戶資料</h1>
{loadingUpdate && <Loader />}
{errorUpdate && <Message variant='danger'>{errorUpdate}</Message>}
)

此時回到網頁,進行用戶資料的編輯,應能正常更新用戶資料,並自動導向到用戶列表頁面:

update_success


若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~


留言版