From f52ee5532dbe56632e9654f432e2bbf16dd69e6b Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Mon, 13 Jan 2020 08:01:42 +0000 Subject: [PATCH] Add UI for admins to view and edit member's details --- apiserver/apiserver/api/models.py | 1 + apiserver/apiserver/api/serializers.py | 10 +- apiserver/import_old_portal.py | 2 + webclient/src/Account.js | 20 +-- webclient/src/Admin.js | 206 +++++++++++++++++++++++++ webclient/src/App.js | 4 +- webclient/src/Classes.js | 2 - webclient/src/Courses.js | 2 - webclient/src/Home.js | 5 + webclient/src/LoginSignup.js | 2 - webclient/src/Members.js | 48 ++++-- webclient/src/Misc.js | 6 +- webclient/src/utils.js | 4 +- 13 files changed, 268 insertions(+), 44 deletions(-) create mode 100644 webclient/src/Admin.js diff --git a/apiserver/apiserver/api/models.py b/apiserver/apiserver/api/models.py index de4d4eb..6b08b4b 100644 --- a/apiserver/apiserver/api/models.py +++ b/apiserver/apiserver/api/models.py @@ -7,6 +7,7 @@ from . import old_models class Member(models.Model): user = models.OneToOneField(User, blank=True, null=True, on_delete=models.SET_NULL) + old_email = models.CharField(max_length=254, blank=True, null=True) photo_large = models.CharField(max_length=64, blank=True, null=True) photo_medium = models.CharField(max_length=64, blank=True, null=True) photo_small = models.CharField(max_length=64, blank=True, null=True) diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py index 735577d..2a6106c 100644 --- a/apiserver/apiserver/api/serializers.py +++ b/apiserver/apiserver/api/serializers.py @@ -55,7 +55,7 @@ class OtherMemberSerializer(serializers.ModelSerializer): class UserEmailField(serializers.ModelField): def to_representation(self, obj): - return obj.user.email + return getattr(obj.user, 'email', obj.old_email) def to_internal_value(self, data): return serializers.EmailField().run_validation(data) @@ -88,8 +88,9 @@ class MemberSerializer(serializers.ModelSerializer): ] def update(self, instance, validated_data): - instance.user.email = validated_data.get('email', instance.user.email) - instance.user.save() + if instance.user: + instance.user.email = validated_data.get('email', instance.user.email) + instance.user.save() photo = validated_data.get('photo', None) if photo: @@ -107,6 +108,7 @@ class AdminMemberSerializer(MemberSerializer): fields = '__all__' read_only_fields = [ 'id', + 'status', 'photo_large', 'photo_medium', 'photo_small', @@ -127,7 +129,7 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['id', 'username', 'member', 'transactions', 'cards', 'training'] + fields = ['id', 'username', 'member', 'transactions', 'cards', 'training', 'is_staff'] depth = 1 diff --git a/apiserver/import_old_portal.py b/apiserver/import_old_portal.py index ff6b917..28b54c7 100755 --- a/apiserver/import_old_portal.py +++ b/apiserver/import_old_portal.py @@ -7,6 +7,7 @@ from apiserver.api.serializers import process_image MEMBER_FIELDS = [ 'id', + # email -> old_email 'first_name', 'last_name', 'preferred_name', @@ -91,6 +92,7 @@ for o in old: if o.city and o.province: new['city'] = '{}, {}'.format(o.city, o.province) + new['old_email'] = o.email new['is_minor'] = o.minor small, medium, large = None, None, None diff --git a/webclient/src/Account.js b/webclient/src/Account.js index 01552f9..e069aad 100644 --- a/webclient/src/Account.js +++ b/webclient/src/Account.js @@ -65,11 +65,7 @@ function ChangePasswordForm(props) { export function AccountForm(props) { const member = props.user.member; - const [input, setInput] = useState({ - ...member, - birthdate: member.birthdate || '', - set_details: true - }); + const [input, setInput] = useState({ ...member, set_details: true }); const [error, setError] = useState({}); const [loading, setLoading] = useState(false); const history = useHistory(); @@ -97,7 +93,7 @@ export function AccountForm(props) { const makeProps = (name) => ({ name: name, onChange: handleChange, - value: input[name], + value: input[name] || '', error: error[name], }); @@ -111,14 +107,14 @@ export function AccountForm(props) { {...makeProps('first_name')} /> Are you under 18 years old? @@ -159,7 +155,7 @@ export function AccountForm(props) { {...makeProps('birthdate')} />} {input.is_minor && } diff --git a/webclient/src/Admin.js b/webclient/src/Admin.js new file mode 100644 index 0000000..32cd943 --- /dev/null +++ b/webclient/src/Admin.js @@ -0,0 +1,206 @@ +import React, { useState, useEffect } from 'react'; +import { BrowserRouter as Router, Switch, Route, Link, useParams, useHistory } from 'react-router-dom'; +import './light.css'; +import { Container, Checkbox, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; +import { BasicTable, staticUrl, requester } from './utils.js'; + +export function AdminMemberForm(props) { + const [input, setInput] = useState(false); + const [error, setError] = useState(false); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const { id } = useParams(); + + const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value }); + const handleUpload = (e, v) => setInput({ ...input, [v.name]: e.target.files[0] }); + const handleChange = (e) => handleValues(e, e.currentTarget); + const handleCheck = (e, v) => setInput({ ...input, [v.name]: v.checked }); + + useEffect(() => { + requester('/members/'+id+'/', 'GET', props.token) + .then(res => { + setInput(res); + }) + .catch(err => { + console.log(err); + setError(true); + }); + }, []); + + const handleSubmit = (e) => { + setLoading(true); + setSuccess(false); + requester('/members/' + id + '/', 'PATCH', props.token, input) + .then(res => { + setLoading(false); + setSuccess(true); + setError(false); + }) + .catch(err => { + setLoading(false); + console.log(err); + setError(err.data); + }); + }; + + const makeProps = (name) => ({ + name: name, + onChange: handleChange, + value: input[name] || '', + error: error[name], + }); + + return ( +
+ {!error ? + input ? +
+
Edit Member Details
+ + + + + + + + + + + + + + + + + + + + + + {success &&

Success!

} + + Submit + + + : +

Loading...

+ : +

Error loading member

+ } +
+ ); +}; + +export function AdminMemberInfo(props) { + const [member, setMember] = useState(false); + const [error, setError] = useState(false); + const { id } = useParams(); + + useEffect(() => { + requester('/members/'+id+'/', 'GET', props.token) + .then(res => { + setMember(res); + }) + .catch(err => { + console.log(err); + setError(true); + }); + }, []); + + return ( +
+ {!error ? + member ? +
+
Admin Details
+ + + + + Name: + {member.first_name} {member.last_name} + + + Status: + {member.status} + + + + Email: + {member.email} + + + Phone: + {member.phone} + + + + Address: + {member.street_address} + + + City: + {member.city} + + + Postal: + {member.postal_code} + + + + Minor: + {member.is_minor ? 'Yes' : 'No'} + + {member.is_minor && + Birthdate: + {member.birthdate} + } + {member.is_minor && + Guardian: + {member.guardian_name} + } + + + Emergency Contact Name: + {member.emergency_contact_name} + + + Emergency Contact Phone: + {member.emergency_contact_phone} + + + +
+ : +

Loading...

+ : +

Error loading member

+ } +
+ ); +}; diff --git a/webclient/src/App.js b/webclient/src/App.js index 357fdd1..bc1cb27 100644 --- a/webclient/src/App.js +++ b/webclient/src/App.js @@ -30,7 +30,6 @@ function App() { useEffect(() => { requester('/user/', 'GET', token) .then(res => { - console.log(res); setUserCache(res); }) .catch(err => { @@ -42,7 +41,6 @@ function App() { function logout() { setTokenCache(''); setUserCache(false); - window.location = '/'; } return ( @@ -163,7 +161,7 @@ function App() { - + diff --git a/webclient/src/Classes.js b/webclient/src/Classes.js index 450d487..2da3be1 100644 --- a/webclient/src/Classes.js +++ b/webclient/src/Classes.js @@ -56,7 +56,6 @@ export function Classes(props) { useEffect(() => { requester('/sessions/', 'GET', token) .then(res => { - console.log(res); setClasses(res.results); }) .catch(err => { @@ -96,7 +95,6 @@ export function ClassDetail(props) { useEffect(() => { requester('/sessions/'+id+'/', 'GET', token) .then(res => { - console.log(res); setClass(res); }) .catch(err => { diff --git a/webclient/src/Courses.js b/webclient/src/Courses.js index 0fb0c33..8437d1f 100644 --- a/webclient/src/Courses.js +++ b/webclient/src/Courses.js @@ -13,7 +13,6 @@ export function Courses(props) { useEffect(() => { requester('/courses/', 'GET', token) .then(res => { - console.log(res); setCourses(res.results); }) .catch(err => { @@ -64,7 +63,6 @@ export function CourseDetail(props) { useEffect(() => { requester('/courses/'+id+'/', 'GET', token) .then(res => { - console.log(res); setCourse(res); }) .catch(err => { diff --git a/webclient/src/Home.js b/webclient/src/Home.js index f903afe..ad4c07e 100644 --- a/webclient/src/Home.js +++ b/webclient/src/Home.js @@ -46,6 +46,11 @@ function MemberInfo(props) { + {!member.photo_medium && + Please set a member photo! +

Visit the account settings page to set one.

+
} +
Details
diff --git a/webclient/src/LoginSignup.js b/webclient/src/LoginSignup.js index e2f96c5..22dc661 100644 --- a/webclient/src/LoginSignup.js +++ b/webclient/src/LoginSignup.js @@ -16,7 +16,6 @@ export function LoginForm(props) { setLoading(true); requester('/rest-auth/login/', 'POST', '', input) .then(res => { - console.log(res); setError({}); props.setTokenCache(res.key); }) @@ -71,7 +70,6 @@ export function SignupForm(props) { input.username = genUsername(); requester('/registration/', 'POST', '', input) .then(res => { - console.log(res); setError({}); props.setTokenCache(res.key); }) diff --git a/webclient/src/Members.js b/webclient/src/Members.js index 669f1f1..9a09ce1 100644 --- a/webclient/src/Members.js +++ b/webclient/src/Members.js @@ -3,8 +3,9 @@ import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-r import './light.css'; import { Button, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Input, Item, Menu, Message, Segment, Table } from 'semantic-ui-react'; import moment from 'moment'; -import { BasicTable, staticUrl, requester } from './utils.js'; +import { isAdmin, BasicTable, staticUrl, requester } from './utils.js'; import { NotFound, PleaseLogin } from './Misc.js'; +import { AdminMemberInfo, AdminMemberForm } from './Admin.js'; export function Members(props) { const [members, setMembers] = useState(false); @@ -75,7 +76,7 @@ export function Members(props) { export function MemberDetail(props) { const [member, setMember] = useState(false); const [error, setError] = useState(false); - const { token } = props; + const { token, user } = props; const { id } = useParams(); useEffect(() => { @@ -96,20 +97,35 @@ export function MemberDetail(props) {
{member.preferred_name} {member.last_name}
- - - - - - Status: - {member.status || 'Unknown'} - - - Joined: - {member.current_start_date || 'Unknown'} - - - + + +

+ +

+ + {isAdmin(user) ? + + : + + + + Status: + {member.status || 'Unknown'} + + + Joined: + {member.current_start_date || 'Unknown'} + + + + } +
+ + + {isAdmin(user) && } + +
+
:

Loading...

diff --git a/webclient/src/Misc.js b/webclient/src/Misc.js index 316cf34..bffaa39 100644 --- a/webclient/src/Misc.js +++ b/webclient/src/Misc.js @@ -7,9 +7,11 @@ export function PleaseLogin() { return ( - You must login before you can do that! + You must login before you can do that!

Visit our login page, then try again.

+ +
); }; @@ -18,7 +20,7 @@ export function NotFound() { return ( - The page you requested can't be found! + The page you requested can't be found!

Visit our home page if you are lost.

diff --git a/webclient/src/utils.js b/webclient/src/utils.js index d9149a6..12fc40f 100644 --- a/webclient/src/utils.js +++ b/webclient/src/utils.js @@ -1,6 +1,8 @@ import React, { useState, useEffect } from 'react'; import { Table } from 'semantic-ui-react'; +export const isAdmin = (user) => user.is_staff || user.member.is_director; + export const siteUrl = window.location.protocol + '//' + window.location.hostname; export const apiUrl = window.location.protocol + '//api.' + window.location.hostname; export const staticUrl = window.location.protocol + '//static.' + window.location.hostname; @@ -23,7 +25,7 @@ export const requester = (route, method, token, data) => { } else if (['POST', 'PUT', 'PATCH'].includes(method)) { const formData = new FormData(); Object.keys(data).forEach(key => - formData.append(key, data[key]) + formData.append(key, data[key] === null ? '' : data[key]) ); options = {