取得 Admin Token
由於在 user_views.py
中,使用 IsAdminUser
來限制只有 Admin 才能取得 User 資料,所以需要先登入 Admin 帳號,取得 Admin 使用者 Token,才能透過 Postman 取得 User 資料。
1 2 3 4 5 6
| @api_view(['GET']) @permission_classes([IsAdminUser]) def getUsers(request): users = User.objects.all() serializer = UserSerializer(users,many=True) return Response(serializer.data)
|
若套用一般使用者的 Token 會出現以下錯誤:
因此先登入 Admin 帳號,取得 Admin 使用者 Token,才能透過 Postman 取得所有 User 的資料。
用戶列表前端實作
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 }; case USER_LIST_FAIL: return { loading: false, error: action.payload }; case USER_LIST_RESET: 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: {
... userList: userListReducer, }, preloadedState: initialState, middleware: middleware, });
|
此時去網頁開啟 Redux DevTools,應能看到多了一個 userList
的 state:
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
|
export const listUsers = (user) => async (dispatch, getState) => { try { dispatch({ type: USER_LIST_REQUEST, });
const { userLogin: { userInfo }, } = getState();
const config = { headers: { "Content-Type": "application/json", Authorization: `Bearer ${userInfo.token}`, }, }; const { data } = await axios.get( `/api/users/`, user, config );
dispatch({ type: USER_LIST_SUCCESS, payload: data, }); } 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
| export const logout = () => (dispatch) => { localStorage.removeItem("userInfo"); dispatch({ type: USER_LOGOUT }); dispatch({ type: USER_DETAILS_RESET }); 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]); ... return( ... )
|
回到 App.js
中註冊 UserListPage
的 Route,測試看頁面是否有成功顯示:
1 2 3 4 5 6 7 8
| import UserListPage from "./Pages/UserListPage"; ... <Routes> <Route path="/" element={<HomePage/>} /> .... <Route path="/admin/userlist" element={<UserListPage/>} /> </Routes>4
|
非 Admin 使用者重新導向至登入頁面
判斷若非 Admin 使用者就直接不顯示這個下拉選項以及不能訪問該頁面 參考這篇 -> 管理員專屬後台頁面-路由結構最佳化寫法 :
製作顯示所有使用者資料的表格
這邊不直接使用 NavDropdown.Item
的 href
,而是使用 LinkContainer
來導向到 /admin/userlist
,是因為要保有 React SPA 的優勢,不要重新整理頁面。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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 來處理編輯、刪除使用者資料。
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']) @permission_classes([IsAdminUser]) 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.js
的 configureStore
中加入 userDeleteReducer
,
1 2 3 4 5 6 7 8 9
| const store = configureStore({ reducer: { ... userDelete: userDeleteReducer, }, preloadedState: initialState, middleware: middleware, });
|
註冊完畢後,在前端開啟 Redux DevTools,應能看到多了一個 userDelete
的 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) => { try { dispatch({ type: USER_DELETE_REQUEST, });
const { userLogin: { userInfo }, } = getState();
const config = { headers: { "Content-Type": "application/json", Authorization: `Bearer ${userInfo.token}`, }, }; const { data } = await axios.delete(`/api/users/delete/${id}/`, config);
dispatch({ type: USER_DELETE_SUCCESS, payload: data, }); } 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
狀態,然後解構 success
為 successDelete
(提取刪除操作是否成功的標誌,並重命名為 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]);
const deleteUserHandler = (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);
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]) def adminGetUserById(request,pk): user = User.objects.get(id=pk) 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']) @permission_classes([IsAuthenticated]) 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.save() return Response(serializer.data)
|
更新 urls.py
1 2 3
| path('<str:pk>/', views.adminGetUserById, name='user_get_by_id'), path('update/<str:pk>/', views.adminUpdateUser, name='user_update'),
|
寫完後去 Postman 測試看看是否有成功取得特定 User 的資料,一樣應要代入 Admin Token 才能取得 User 資料。
前端實作
編輯使用者頁面
建立一個名為 UserEditPage.js
的頁面,功能部分類似於註冊頁面
- 透過
useParams
取得 URL 中用戶的 id
,記得在App.js
中若是定義:userID
,那用 useParams
取時也一樣要用
userID
,而不是 id
1 2 3
| ... <Route path='/admin/user/:userID/edit' element={<AdminRoute><UserEditPage /></AdminRoute>} />
|
- 透過
useDispatch
來 dispatch getUserDetails
action 來取得特定 User 的資料
- 透過
useSelector
來取得 userDetails
的狀態,並解構出 loading
、error
、user
三個屬性
1 2 3 4 5 6 7 8 9 10 11
| const UserEditPage = () => { const { userID } = useParams(); }; ... const userDetails = useSelector((state) => state.userDetails); const { loading, error, user } = userDetails;
const submitHandler = (e) => { e.preventDefault(); };
|
如果沒用 Number
將userID
轉換成數字,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)); } 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> )} </> );
|
解決從編輯用戶資料跳至個人資料頁面,資料顯示不正確的問題
目前若從用戶編輯頁面
跳至用戶個人資料頁面,會發現資料載入不正確,必須要重新整理頁面才能正確載入。
那這是由於在 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
| useEffect(() => { ... if (!user || !user.first_name || success || userInfo.id !== user.id) { ... } ... }, [dispatch, userInfo, user, success]);
|
Constants 定義
1 2 3 4
| 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
| 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: [] }; default: return state; } };
|
然後一樣至 store.js
的 configureStore
中加入 userUpdateReducer
,並回瀏覽器的 Redux DevTools
中檢查是否有 userUpdate
的 state。
Actions 實作
「更新操作的重點在於需要修改的資料,而不是指定哪個用戶」
updateUser
函式改傳入 user
而不是 id
,因為更新需要多個欄位(例如名字、電子郵件、角色、狀態等),所以傳入整個 user
物件
axios
使用的是 put
方法,並帶入 user
物件,路由所帶入的 id
是 user.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) => { try { dispatch({ type: USER_UPDATE_REQUEST, });
const { userLogin: { userInfo }, } = getState();
const config = { headers: { "Content-Type": "application/json", Authorization: `Bearer ${userInfo.token}`, }, }; 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
| import { ..., updateUser } from "../actions/userActions"; import { USER_UPDATE_RESET } from "../constants/userConstants";
const userUpdate = useSelector((state) => state.userUpdate);
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)); } 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>} )
|
此時回到網頁,進行用戶資料的編輯,應能正常更新用戶資料,並自動導向到用戶列表頁面:
若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~
留言版