Add UI for admins to view and edit member's details

This commit is contained in:
Tanner Collin 2020-01-13 08:01:42 +00:00
parent 0c814790a7
commit f52ee5532d
13 changed files with 268 additions and 44 deletions

View File

@ -7,6 +7,7 @@ from . import old_models
class Member(models.Model): class Member(models.Model):
user = models.OneToOneField(User, blank=True, null=True, on_delete=models.SET_NULL) 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_large = models.CharField(max_length=64, blank=True, null=True)
photo_medium = 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) photo_small = models.CharField(max_length=64, blank=True, null=True)

View File

@ -55,7 +55,7 @@ class OtherMemberSerializer(serializers.ModelSerializer):
class UserEmailField(serializers.ModelField): class UserEmailField(serializers.ModelField):
def to_representation(self, obj): def to_representation(self, obj):
return obj.user.email return getattr(obj.user, 'email', obj.old_email)
def to_internal_value(self, data): def to_internal_value(self, data):
return serializers.EmailField().run_validation(data) return serializers.EmailField().run_validation(data)
@ -88,6 +88,7 @@ class MemberSerializer(serializers.ModelSerializer):
] ]
def update(self, instance, validated_data): def update(self, instance, validated_data):
if instance.user:
instance.user.email = validated_data.get('email', instance.user.email) instance.user.email = validated_data.get('email', instance.user.email)
instance.user.save() instance.user.save()
@ -107,6 +108,7 @@ class AdminMemberSerializer(MemberSerializer):
fields = '__all__' fields = '__all__'
read_only_fields = [ read_only_fields = [
'id', 'id',
'status',
'photo_large', 'photo_large',
'photo_medium', 'photo_medium',
'photo_small', 'photo_small',
@ -127,7 +129,7 @@ class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['id', 'username', 'member', 'transactions', 'cards', 'training'] fields = ['id', 'username', 'member', 'transactions', 'cards', 'training', 'is_staff']
depth = 1 depth = 1

View File

@ -7,6 +7,7 @@ from apiserver.api.serializers import process_image
MEMBER_FIELDS = [ MEMBER_FIELDS = [
'id', 'id',
# email -> old_email
'first_name', 'first_name',
'last_name', 'last_name',
'preferred_name', 'preferred_name',
@ -91,6 +92,7 @@ for o in old:
if o.city and o.province: if o.city and o.province:
new['city'] = '{}, {}'.format(o.city, o.province) new['city'] = '{}, {}'.format(o.city, o.province)
new['old_email'] = o.email
new['is_minor'] = o.minor new['is_minor'] = o.minor
small, medium, large = None, None, None small, medium, large = None, None, None

View File

@ -65,11 +65,7 @@ function ChangePasswordForm(props) {
export function AccountForm(props) { export function AccountForm(props) {
const member = props.user.member; const member = props.user.member;
const [input, setInput] = useState({ const [input, setInput] = useState({ ...member, set_details: true });
...member,
birthdate: member.birthdate || '',
set_details: true
});
const [error, setError] = useState({}); const [error, setError] = useState({});
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const history = useHistory(); const history = useHistory();
@ -97,7 +93,7 @@ export function AccountForm(props) {
const makeProps = (name) => ({ const makeProps = (name) => ({
name: name, name: name,
onChange: handleChange, onChange: handleChange,
value: input[name], value: input[name] || '',
error: error[name], error: error[name],
}); });
@ -110,16 +106,16 @@ export function AccountForm(props) {
required required
{...makeProps('first_name')} {...makeProps('first_name')}
/> />
<Form.Input
label='Last Name'
required
{...makeProps('last_name')}
/>
<Form.Input <Form.Input
label='Preferred First Name' label='Preferred First Name'
required required
{...makeProps('preferred_name')} {...makeProps('preferred_name')}
/> />
<Form.Input
label='Last Name'
required
{...makeProps('last_name')}
/>
<Form.Input <Form.Input
label='Email Address' label='Email Address'
@ -148,7 +144,7 @@ export function AccountForm(props) {
<label>Are you under 18 years old?</label> <label>Are you under 18 years old?</label>
<Checkbox <Checkbox
label='I am a minor' label='I am a minor'
{...makeProps('is_minor')} name='is_minor'
onChange={handleCheck} onChange={handleCheck}
checked={input.is_minor} checked={input.is_minor}
/> />
@ -159,7 +155,7 @@ export function AccountForm(props) {
{...makeProps('birthdate')} {...makeProps('birthdate')}
/>} />}
{input.is_minor && <Form.Input {input.is_minor && <Form.Input
label="Parent's Name" label="Guardian's Name"
{...makeProps('guardian_name')} {...makeProps('guardian_name')}
/>} />}

206
webclient/src/Admin.js Normal file
View File

@ -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 (
<div>
{!error ?
input ?
<Form onSubmit={handleSubmit}>
<Header size='medium'>Edit Member Details</Header>
<Form.Input
label='Application Date'
{...makeProps('application_date')}
/>
<Form.Input
label='Current Start Date'
{...makeProps('current_start_date')}
/>
<Form.Input
label='Vetted Date'
{...makeProps('vetted_date')}
/>
<Form.Input
label='Expire Date'
{...makeProps('Expire Date')}
/>
<Form.Input
label='Membership Fee'
{...makeProps('monthly_fees')}
/>
<Form.Field>
<label>Is the member a director?</label>
<Checkbox
label='Yes'
name='is_director'
onChange={handleCheck}
checked={input.is_director}
/>
</Form.Field>
<Form.Field>
<label>Is the member an instructor?</label>
<Checkbox
label='Yes'
name='is_instructor'
onChange={handleCheck}
checked={input.is_instructor}
/>
</Form.Field>
{success && <p>Success!</p>}
<Form.Button loading={loading} error={error.non_field_errors}>
Submit
</Form.Button>
</Form>
:
<p>Loading...</p>
:
<p>Error loading member</p>
}
</div>
);
};
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 (
<div>
{!error ?
member ?
<div>
<Header size='medium'>Admin Details</Header>
<BasicTable>
<Table.Body>
<Table.Row>
<Table.Cell>Name:</Table.Cell>
<Table.Cell>{member.first_name} {member.last_name}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Status:</Table.Cell>
<Table.Cell>{member.status}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Email:</Table.Cell>
<Table.Cell>{member.email}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Phone:</Table.Cell>
<Table.Cell>{member.phone}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Address:</Table.Cell>
<Table.Cell>{member.street_address}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>City:</Table.Cell>
<Table.Cell>{member.city}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Postal:</Table.Cell>
<Table.Cell>{member.postal_code}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Minor:</Table.Cell>
<Table.Cell>{member.is_minor ? 'Yes' : 'No'}</Table.Cell>
</Table.Row>
{member.is_minor && <Table.Row>
<Table.Cell>Birthdate:</Table.Cell>
<Table.Cell>{member.birthdate}</Table.Cell>
</Table.Row>}
{member.is_minor && <Table.Row>
<Table.Cell>Guardian:</Table.Cell>
<Table.Cell>{member.guardian_name}</Table.Cell>
</Table.Row>}
<Table.Row>
<Table.Cell>Emergency Contact Name:</Table.Cell>
<Table.Cell>{member.emergency_contact_name}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Emergency Contact Phone:</Table.Cell>
<Table.Cell>{member.emergency_contact_phone}</Table.Cell>
</Table.Row>
</Table.Body>
</BasicTable>
</div>
:
<p>Loading...</p>
:
<p>Error loading member</p>
}
</div>
);
};

View File

@ -30,7 +30,6 @@ function App() {
useEffect(() => { useEffect(() => {
requester('/user/', 'GET', token) requester('/user/', 'GET', token)
.then(res => { .then(res => {
console.log(res);
setUserCache(res); setUserCache(res);
}) })
.catch(err => { .catch(err => {
@ -42,7 +41,6 @@ function App() {
function logout() { function logout() {
setTokenCache(''); setTokenCache('');
setUserCache(false); setUserCache(false);
window.location = '/';
} }
return ( return (
@ -163,7 +161,7 @@ function App() {
</Route> </Route>
<Route path='/members/:id'> <Route path='/members/:id'>
<MemberDetail token={token} /> <MemberDetail token={token} user={user} />
</Route> </Route>
<Route path='/members'> <Route path='/members'>
<Members token={token} /> <Members token={token} />

View File

@ -56,7 +56,6 @@ export function Classes(props) {
useEffect(() => { useEffect(() => {
requester('/sessions/', 'GET', token) requester('/sessions/', 'GET', token)
.then(res => { .then(res => {
console.log(res);
setClasses(res.results); setClasses(res.results);
}) })
.catch(err => { .catch(err => {
@ -96,7 +95,6 @@ export function ClassDetail(props) {
useEffect(() => { useEffect(() => {
requester('/sessions/'+id+'/', 'GET', token) requester('/sessions/'+id+'/', 'GET', token)
.then(res => { .then(res => {
console.log(res);
setClass(res); setClass(res);
}) })
.catch(err => { .catch(err => {

View File

@ -13,7 +13,6 @@ export function Courses(props) {
useEffect(() => { useEffect(() => {
requester('/courses/', 'GET', token) requester('/courses/', 'GET', token)
.then(res => { .then(res => {
console.log(res);
setCourses(res.results); setCourses(res.results);
}) })
.catch(err => { .catch(err => {
@ -64,7 +63,6 @@ export function CourseDetail(props) {
useEffect(() => { useEffect(() => {
requester('/courses/'+id+'/', 'GET', token) requester('/courses/'+id+'/', 'GET', token)
.then(res => { .then(res => {
console.log(res);
setCourse(res); setCourse(res);
}) })
.catch(err => { .catch(err => {

View File

@ -46,6 +46,11 @@ function MemberInfo(props) {
</Grid.Column> </Grid.Column>
</Grid> </Grid>
{!member.photo_medium && <Message warning>
<Message.Header>Please set a member photo!</Message.Header>
<p>Visit the <Link to='/account'>account settings</Link> page to set one.</p>
</Message>}
<Header size='medium'>Details</Header> <Header size='medium'>Details</Header>
<BasicTable> <BasicTable>
<Table.Body> <Table.Body>

View File

@ -16,7 +16,6 @@ export function LoginForm(props) {
setLoading(true); setLoading(true);
requester('/rest-auth/login/', 'POST', '', input) requester('/rest-auth/login/', 'POST', '', input)
.then(res => { .then(res => {
console.log(res);
setError({}); setError({});
props.setTokenCache(res.key); props.setTokenCache(res.key);
}) })
@ -71,7 +70,6 @@ export function SignupForm(props) {
input.username = genUsername(); input.username = genUsername();
requester('/registration/', 'POST', '', input) requester('/registration/', 'POST', '', input)
.then(res => { .then(res => {
console.log(res);
setError({}); setError({});
props.setTokenCache(res.key); props.setTokenCache(res.key);
}) })

View File

@ -3,8 +3,9 @@ import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-r
import './light.css'; import './light.css';
import { Button, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Input, Item, Menu, Message, Segment, Table } from 'semantic-ui-react'; 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 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 { NotFound, PleaseLogin } from './Misc.js';
import { AdminMemberInfo, AdminMemberForm } from './Admin.js';
export function Members(props) { export function Members(props) {
const [members, setMembers] = useState(false); const [members, setMembers] = useState(false);
@ -75,7 +76,7 @@ export function Members(props) {
export function MemberDetail(props) { export function MemberDetail(props) {
const [member, setMember] = useState(false); const [member, setMember] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const { token } = props; const { token, user } = props;
const { id } = useParams(); const { id } = useParams();
useEffect(() => { useEffect(() => {
@ -96,8 +97,15 @@ export function MemberDetail(props) {
<div> <div>
<Header size='large'>{member.preferred_name} {member.last_name}</Header> <Header size='large'>{member.preferred_name} {member.last_name}</Header>
<Grid stackable columns={2}>
<Grid.Column>
<p>
<Image rounded size='medium' src={member.photo_large ? staticUrl + '/' + member.photo_large : '/nophoto.png'} /> <Image rounded size='medium' src={member.photo_large ? staticUrl + '/' + member.photo_large : '/nophoto.png'} />
</p>
{isAdmin(user) ?
<AdminMemberInfo {...props} />
:
<BasicTable> <BasicTable>
<Table.Body> <Table.Body>
<Table.Row> <Table.Row>
@ -110,6 +118,14 @@ export function MemberDetail(props) {
</Table.Row> </Table.Row>
</Table.Body> </Table.Body>
</BasicTable> </BasicTable>
}
</Grid.Column>
<Grid.Column>
{isAdmin(user) && <Segment padded><AdminMemberForm {...props} /></Segment>}
</Grid.Column>
</Grid>
</div> </div>
: :
<p>Loading...</p> <p>Loading...</p>

View File

@ -7,9 +7,11 @@ export function PleaseLogin() {
return ( return (
<Container text> <Container text>
<Message warning> <Message warning>
<Message.Header style={{ padding: 0 }}>You must login before you can do that!</Message.Header> <Message.Header>You must login before you can do that!</Message.Header>
<p>Visit our <Link to='/'>login page</Link>, then try again.</p> <p>Visit our <Link to='/'>login page</Link>, then try again.</p>
</Message> </Message>
<img className='photo-404' src='/404.jpg' />
</Container> </Container>
); );
}; };
@ -18,7 +20,7 @@ export function NotFound() {
return ( return (
<Container text> <Container text>
<Message warning> <Message warning>
<Message.Header style={{ padding: 0 }}>The page you requested can't be found!</Message.Header> <Message.Header>The page you requested can't be found!</Message.Header>
<p>Visit our <Link to='/'>home page</Link> if you are lost.</p> <p>Visit our <Link to='/'>home page</Link> if you are lost.</p>
</Message> </Message>

View File

@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Table } from 'semantic-ui-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 siteUrl = window.location.protocol + '//' + window.location.hostname;
export const apiUrl = window.location.protocol + '//api.' + window.location.hostname; export const apiUrl = window.location.protocol + '//api.' + window.location.hostname;
export const staticUrl = window.location.protocol + '//static.' + 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)) { } else if (['POST', 'PUT', 'PATCH'].includes(method)) {
const formData = new FormData(); const formData = new FormData();
Object.keys(data).forEach(key => Object.keys(data).forEach(key =>
formData.append(key, data[key]) formData.append(key, data[key] === null ? '' : data[key])
); );
options = { options = {