partial #115
enable members to vouch for each other enable admin to view vouching info on all users
This commit is contained in:
parent
6bab989d42
commit
a0f9007d37
|
@ -19,6 +19,7 @@ def today_alberta_tz():
|
||||||
class Member(models.Model):
|
class Member(models.Model):
|
||||||
user = models.OneToOneField(User, related_name='member', blank=True, null=True, on_delete=models.SET_NULL)
|
user = models.OneToOneField(User, related_name='member', blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
signup_helper = models.ForeignKey(User, related_name='signed_up', blank=True, null=True, on_delete=models.SET_NULL)
|
signup_helper = models.ForeignKey(User, related_name='signed_up', blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
|
sponsorship = models.ManyToManyField('self', related_name='sponsored_by', symmetrical=False, blank=True)
|
||||||
old_email = models.CharField(max_length=254, blank=True, null=True)
|
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)
|
||||||
|
|
|
@ -248,6 +248,8 @@ class MemberSerializer(serializers.ModelSerializer):
|
||||||
email = fields.UserEmailField(serializers.EmailField)
|
email = fields.UserEmailField(serializers.EmailField)
|
||||||
phone = serializers.CharField()
|
phone = serializers.CharField()
|
||||||
protocoin = serializers.SerializerMethodField()
|
protocoin = serializers.SerializerMethodField()
|
||||||
|
sponsorship = OtherMemberSerializer(many=True, read_only=True)
|
||||||
|
sponsored_by = OtherMemberSerializer(many=True, read_only=True)
|
||||||
total_protocoin = serializers.SerializerMethodField()
|
total_protocoin = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -1895,6 +1895,24 @@ class StorageSpaceViewSet(Base, List, Retrieve, Update):
|
||||||
return Response(200)
|
return Response(200)
|
||||||
|
|
||||||
|
|
||||||
|
class SponsorshipViewSet(Base):
|
||||||
|
permission_classes = [AllowMetadata | IsAuthenticated]
|
||||||
|
queryset = models.Member.objects.all()
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def offer(self, request, pk=None):
|
||||||
|
value = self.request.data['value']
|
||||||
|
member = self.get_object()
|
||||||
|
sponsor = self.request.user.member
|
||||||
|
if value == 'true':
|
||||||
|
sponsor.sponsorship.add(member.id)
|
||||||
|
logging.info('Member %s is now sponsoring member %s', sponsor.preferred_name, member.preferred_name)
|
||||||
|
else:
|
||||||
|
sponsor.sponsorship.remove(member.id)
|
||||||
|
logging.info('Member %s revoked sponsorship for member %s', sponsor.preferred_name, member.preferred_name)
|
||||||
|
sponsor.save()
|
||||||
|
return Response(200)
|
||||||
|
|
||||||
class RegistrationView(RegisterView):
|
class RegistrationView(RegisterView):
|
||||||
serializer_class = serializers.MyRegisterSerializer
|
serializer_class = serializers.MyRegisterSerializer
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ router.register(r'members', views.MemberViewSet, basename='members')
|
||||||
router.register(r'courses', views.CourseViewSet, basename='course')
|
router.register(r'courses', views.CourseViewSet, basename='course')
|
||||||
router.register(r'history', views.HistoryViewSet, basename='history')
|
router.register(r'history', views.HistoryViewSet, basename='history')
|
||||||
router.register(r'vetting', views.VettingViewSet, basename='vetting')
|
router.register(r'vetting', views.VettingViewSet, basename='vetting')
|
||||||
|
router.register(r'sponsorship', views.SponsorshipViewSet, basename='sponsorship')
|
||||||
router.register(r'pinball', views.PinballViewSet, basename='pinball')
|
router.register(r'pinball', views.PinballViewSet, basename='pinball')
|
||||||
router.register(r'storage', views.StorageSpaceViewSet, basename='storage')
|
router.register(r'storage', views.StorageSpaceViewSet, basename='storage')
|
||||||
router.register(r'hosting', views.HostingViewSet, basename='hosting')
|
router.register(r'hosting', views.HostingViewSet, basename='hosting')
|
||||||
|
|
|
@ -6,7 +6,9 @@ import 'react-image-crop/dist/ReactCrop.css';
|
||||||
import './light.css';
|
import './light.css';
|
||||||
import { MembersDropdown } from './Members.js';
|
import { MembersDropdown } from './Members.js';
|
||||||
import { Button, Container, Form, Grid, Header, Message, Segment } from 'semantic-ui-react';
|
import { Button, Container, Form, Grid, Header, Message, Segment } from 'semantic-ui-react';
|
||||||
|
import './components/MembersList'
|
||||||
import { requester, randomString } from './utils.js';
|
import { requester, randomString } from './utils.js';
|
||||||
|
import { MembersList } from './components/MembersList';
|
||||||
|
|
||||||
function LogoutEverywhere(props) {
|
function LogoutEverywhere(props) {
|
||||||
const { token } = props;
|
const { token } = props;
|
||||||
|
@ -335,6 +337,19 @@ export function AccountForm(props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function Sponsorship(props) {
|
||||||
|
const { user: { member } } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Header size='medium'>My sponsors:</Header>
|
||||||
|
<MembersList list={ member.sponsored_by }/>
|
||||||
|
<Header size='medium'>I am sponsoring:</Header>
|
||||||
|
<MembersList list={ member.sponsorship }/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function BioNotesForm(props) {
|
export function BioNotesForm(props) {
|
||||||
const { token, user, refreshUser } = props;
|
const { token, user, refreshUser } = props;
|
||||||
const member = user.member;
|
const member = user.member;
|
||||||
|
@ -401,6 +416,7 @@ export function Account(props) {
|
||||||
<Grid stackable columns={2}>
|
<Grid stackable columns={2}>
|
||||||
<Grid.Column>
|
<Grid.Column>
|
||||||
<Segment padded><AccountForm {...props} /></Segment>
|
<Segment padded><AccountForm {...props} /></Segment>
|
||||||
|
<Segment padded><Sponsorship {...props} /></Segment>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
<Grid.Column>
|
<Grid.Column>
|
||||||
<Segment padded><BioNotesForm {...props} /></Segment>
|
<Segment padded><BioNotesForm {...props} /></Segment>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import './light.css';
|
import './light.css';
|
||||||
import { Button, Checkbox, Dimmer, Form, Message, Header, Icon, Image, Segment, Table } from 'semantic-ui-react';
|
import { Button, Checkbox, Dimmer, Form, Message, Header, Icon, Image, Segment, Table, List, ListItem } from 'semantic-ui-react';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { statusColor, BasicTable, staticUrl, requester } from './utils.js';
|
import { statusColor, BasicTable, staticUrl, requester } from './utils.js';
|
||||||
import { TrainingList } from './Training.js';
|
import { TrainingList } from './Training.js';
|
||||||
|
import { MembersList } from './components/MembersList';
|
||||||
|
|
||||||
function AdminCardDetail(props) {
|
function AdminCardDetail(props) {
|
||||||
const { token, result, card } = props;
|
const { token, result, card } = props;
|
||||||
|
@ -615,6 +616,18 @@ export function AdminMemberInfo(props) {
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
|
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell>Vouched by:</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<MembersList list={ member.sponsored_by }/>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell>Vouches for:</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<MembersList list={ member.sponsorship }/>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell>Public Bio:</Table.Cell>
|
<Table.Cell>Public Bio:</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
|
|
|
@ -334,7 +334,7 @@ function App() {
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path='/members/:id'>
|
<Route path='/members/:id'>
|
||||||
<MemberDetail token={token} user={user} />
|
<MemberDetail token={token} user={user} setUser={setUserCache}/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path='/members'>
|
<Route path='/members'>
|
||||||
<Members token={token} user={user} />
|
<Members token={token} user={user} />
|
||||||
|
|
|
@ -282,11 +282,16 @@ export function Members(props) {
|
||||||
let resultCache = {};
|
let resultCache = {};
|
||||||
|
|
||||||
export function MemberDetail(props) {
|
export function MemberDetail(props) {
|
||||||
const { id } = useParams();
|
const id = parseInt(useParams().id)
|
||||||
const [result, setResult] = useState(resultCache[id] || false);
|
const [result, setResult] = useState(resultCache[id] || false);
|
||||||
const [refreshCount, refreshResult] = useReducer(x => x + 1, 0);
|
const [refreshCount, refreshResult] = useReducer(x => x + 1, 0);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const { token, user } = props;
|
const { token, user } = props;
|
||||||
|
const member = result.member || false;
|
||||||
|
const memberFullName = [member.preferred_name, member.last_name].join(' ')
|
||||||
|
const isSponsoring = user.member.sponsorship?.find(m => m.id === id)
|
||||||
|
const isMe = user.member.id === id
|
||||||
|
const photo = member?.photo_large || member?.photo_small || false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
requester('/search/'+id+'/', 'GET', token)
|
requester('/search/'+id+'/', 'GET', token)
|
||||||
|
@ -300,8 +305,23 @@ export function MemberDetail(props) {
|
||||||
});
|
});
|
||||||
}, [refreshCount]);
|
}, [refreshCount]);
|
||||||
|
|
||||||
const member = result.member || false;
|
|
||||||
const photo = member?.photo_large || member?.photo_small || false;
|
function sponsorMember (value) {
|
||||||
|
return () => {
|
||||||
|
requester(`/sponsorship/${id}/offer/`, 'POST', token, { value })
|
||||||
|
.then(res => {
|
||||||
|
const _user = { ...user }
|
||||||
|
const sponsorship = _user.member.sponsorship
|
||||||
|
if (value) sponsorship.push({ id })
|
||||||
|
else sponsorship.splice(sponsorship.findIndex(m => m.id === id), 1)
|
||||||
|
props.setUser(_user)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
setError(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
@ -342,6 +362,8 @@ export function MemberDetail(props) {
|
||||||
<p className='bio-paragraph'>
|
<p className='bio-paragraph'>
|
||||||
{member.public_bio || 'None yet.'}
|
{member.public_bio || 'None yet.'}
|
||||||
</p>
|
</p>
|
||||||
|
{ !isMe && !isSponsoring && <Button onClick={ sponsorMember(true) }>Vouch for { member.preferred_name }</Button> }
|
||||||
|
{ !isMe && isSponsoring && <Button onClick={ sponsorMember(false) }>Revoke guarantee</Button> }
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
|
@ -386,4 +408,3 @@ export function MemberDetail(props) {
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
19
webclient/src/components/MembersList.jsx
Normal file
19
webclient/src/components/MembersList.jsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { List, ListItem } from 'semantic-ui-react';
|
||||||
|
|
||||||
|
export function MembersList (p) {
|
||||||
|
const list = p.list
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
{ list.map(_member => (
|
||||||
|
<ListItem key={'member' + _member.id}>
|
||||||
|
<Link to={'/members/' + _member.id + '/'}>
|
||||||
|
{_member.preferred_name} {_member.last_name}
|
||||||
|
</Link>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user