Import old photos and link to members
This commit is contained in:
parent
5091ff2977
commit
946c70d776
|
@ -21,6 +21,9 @@ class Member(models.Model):
|
||||||
monthly_fees = models.IntegerField(default=55, blank=True, null=True)
|
monthly_fees = models.IntegerField(default=55, blank=True, null=True)
|
||||||
emergency_contact_name = models.CharField(max_length=64, blank=True)
|
emergency_contact_name = models.CharField(max_length=64, blank=True)
|
||||||
emergency_contact_phone = models.CharField(max_length=32, blank=True)
|
emergency_contact_phone = models.CharField(max_length=32, blank=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)
|
||||||
|
|
||||||
class Transaction(models.Model):
|
class Transaction(models.Model):
|
||||||
user = models.ForeignKey(User, related_name='transactions', blank=True, null=True, on_delete=models.SET_NULL)
|
user = models.ForeignKey(User, related_name='transactions', blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
|
|
|
@ -2,11 +2,46 @@ from django.contrib.auth.models import User, Group
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_auth.registration.serializers import RegisterSerializer
|
from rest_auth.registration.serializers import RegisterSerializer
|
||||||
|
from uuid import uuid4
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from . import models, old_models
|
from . import models, old_models
|
||||||
|
|
||||||
#custom_error = lambda x: ValidationError(dict(non_field_errors=x))
|
#custom_error = lambda x: ValidationError(dict(non_field_errors=x))
|
||||||
|
|
||||||
|
STATIC_FOLDER = 'data/static/'
|
||||||
|
LARGE_SIZE = 1080
|
||||||
|
MEDIUM_SIZE = 150
|
||||||
|
SMALL_SIZE = 80
|
||||||
|
|
||||||
|
def process_image(upload):
|
||||||
|
try:
|
||||||
|
pic = Image.open(upload)
|
||||||
|
except OSError:
|
||||||
|
raise serializers.ValidationError('Invalid image file.')
|
||||||
|
|
||||||
|
if pic.format == 'PNG':
|
||||||
|
ext = '.png'
|
||||||
|
elif pic.format == 'JPEG':
|
||||||
|
ext = '.jpg'
|
||||||
|
else:
|
||||||
|
raise serializers.ValidationError('Image must be a jpg or png.')
|
||||||
|
|
||||||
|
large = str(uuid4()) + ext
|
||||||
|
pic.thumbnail([LARGE_SIZE, LARGE_SIZE], Image.ANTIALIAS)
|
||||||
|
pic.save(STATIC_FOLDER + large)
|
||||||
|
|
||||||
|
medium = str(uuid4()) + ext
|
||||||
|
pic.thumbnail([MEDIUM_SIZE, MEDIUM_SIZE], Image.ANTIALIAS)
|
||||||
|
pic.save(STATIC_FOLDER + medium)
|
||||||
|
|
||||||
|
small = str(uuid4()) + ext
|
||||||
|
pic.thumbnail([SMALL_SIZE, SMALL_SIZE], Image.ANTIALIAS)
|
||||||
|
pic.save(STATIC_FOLDER + small)
|
||||||
|
|
||||||
|
return small, medium, large
|
||||||
|
|
||||||
|
|
||||||
class UserTrainingSerializer(serializers.ModelSerializer):
|
class UserTrainingSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Training
|
model = models.Training
|
||||||
|
@ -29,7 +64,7 @@ class OtherMemberSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Member
|
model = models.Member
|
||||||
fields = ['q', 'seq', 'preferred_name', 'last_name', 'status', 'current_start_date']
|
fields = ['q', 'seq', 'preferred_name', 'last_name', 'status', 'current_start_date', 'photo_small']
|
||||||
|
|
||||||
# member viewing himself
|
# member viewing himself
|
||||||
class MemberSerializer(serializers.ModelSerializer):
|
class MemberSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
@ -16,9 +16,6 @@ class AllowMetadata(permissions.BasePermission):
|
||||||
|
|
||||||
search_strings = {}
|
search_strings = {}
|
||||||
def gen_search_strings():
|
def gen_search_strings():
|
||||||
import time
|
|
||||||
start = time.time()
|
|
||||||
|
|
||||||
for m in models.Member.objects.all():
|
for m in models.Member.objects.all():
|
||||||
string = '{} {}'.format(
|
string = '{} {}'.format(
|
||||||
m.preferred_name,
|
m.preferred_name,
|
||||||
|
@ -26,9 +23,6 @@ def gen_search_strings():
|
||||||
).lower()
|
).lower()
|
||||||
search_strings[string] = m.id
|
search_strings[string] = m.id
|
||||||
|
|
||||||
print('Generated search strings in {} s'.format(time.time() - start))
|
|
||||||
gen_search_strings()
|
|
||||||
|
|
||||||
class SearchViewSet(viewsets.ViewSet):
|
class SearchViewSet(viewsets.ViewSet):
|
||||||
permission_classes = [AllowMetadata | permissions.IsAuthenticated]
|
permission_classes = [AllowMetadata | permissions.IsAuthenticated]
|
||||||
serializer_class = serializers.OtherMemberSerializer
|
serializer_class = serializers.OtherMemberSerializer
|
||||||
|
@ -60,6 +54,7 @@ class SearchViewSet(viewsets.ViewSet):
|
||||||
|
|
||||||
queryset = result_objects
|
queryset = result_objects
|
||||||
else:
|
else:
|
||||||
|
gen_search_strings()
|
||||||
queryset = queryset.order_by('-vetted_date')[:NUM_SEARCH_RESULTS]
|
queryset = queryset.order_by('-vetted_date')[:NUM_SEARCH_RESULTS]
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
@ -93,13 +88,6 @@ class MemberViewSet(viewsets.ModelViewSet):
|
||||||
else:
|
else:
|
||||||
return serializers.MemberSerializer
|
return serializers.MemberSerializer
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
|
||||||
gen_search_strings()
|
|
||||||
return super().update(request, *args, **kwargs)
|
|
||||||
def partial_update(self, request, *args, **kwargs):
|
|
||||||
gen_search_strings()
|
|
||||||
return super().partial_update(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CourseViewSet(viewsets.ModelViewSet):
|
class CourseViewSet(viewsets.ModelViewSet):
|
||||||
permission_classes = [AllowMetadata | permissions.IsAuthenticated]
|
permission_classes = [AllowMetadata | permissions.IsAuthenticated]
|
||||||
|
|
|
@ -3,6 +3,7 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'apiserver.settings'
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
from apiserver.api import models, old_models
|
from apiserver.api import models, old_models
|
||||||
|
from apiserver.api.serializers import process_image
|
||||||
|
|
||||||
MEMBER_FIELDS = [
|
MEMBER_FIELDS = [
|
||||||
'id',
|
'id',
|
||||||
|
@ -67,6 +68,10 @@ TRAINING_FIELDS = [
|
||||||
'paid_date',
|
'paid_date',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
photo_folders = os.listdir('old_photos')
|
||||||
|
print('Found {} member photo folders'.format(len(photo_folders)))
|
||||||
|
|
||||||
|
|
||||||
print('Deleting all members...')
|
print('Deleting all members...')
|
||||||
models.Member.objects.all().delete()
|
models.Member.objects.all().delete()
|
||||||
print('Importing old members...')
|
print('Importing old members...')
|
||||||
|
@ -78,7 +83,14 @@ for o in old:
|
||||||
for f in MEMBER_FIELDS:
|
for f in MEMBER_FIELDS:
|
||||||
new[f] = o.__dict__.get(f, None)
|
new[f] = o.__dict__.get(f, None)
|
||||||
|
|
||||||
models.Member.objects.create(**new)
|
small, medium, large = None, None, None
|
||||||
|
if str(o.id) in photo_folders:
|
||||||
|
folder = 'old_photos/' + str(o.id)
|
||||||
|
if 'photo.jpg' in os.listdir(folder):
|
||||||
|
small, medium, large = process_image(folder + '/photo.jpg')
|
||||||
|
print('Found a photo')
|
||||||
|
|
||||||
|
models.Member.objects.create(photo_small=small, photo_medium=medium, photo_large=large, **new)
|
||||||
print('Imported member #{} - {} {}'.format(
|
print('Imported member #{} - {} {}'.format(
|
||||||
o.id, o.first_name, o.last_name
|
o.id, o.first_name, o.last_name
|
||||||
))
|
))
|
||||||
|
|
BIN
webclient/public/nophoto.png
Normal file
BIN
webclient/public/nophoto.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 212 KiB |
|
@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||||
import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom';
|
import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom';
|
||||||
import './light.css';
|
import './light.css';
|
||||||
import { Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react';
|
import { Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react';
|
||||||
import { requester } from './utils.js';
|
import { staticUrl, requester } from './utils.js';
|
||||||
import { LoginForm, SignupForm } from './LoginSignup.js';
|
import { LoginForm, SignupForm } from './LoginSignup.js';
|
||||||
|
|
||||||
function DetailsForm(props) {
|
function DetailsForm(props) {
|
||||||
|
@ -89,7 +89,10 @@ function MemberInfo(props) {
|
||||||
<div>
|
<div>
|
||||||
<Grid stackable>
|
<Grid stackable>
|
||||||
<Grid.Column width={6}>
|
<Grid.Column width={6}>
|
||||||
<Image src='https://upload.wikimedia.org/wikipedia/commons/8/89/Portrait_Placeholder.png' size='small' />
|
<img
|
||||||
|
className='photo-medium'
|
||||||
|
src={member.photo_medium ? staticUrl + '/' + member.photo_medium : 'nophoto.png'} size='small'
|
||||||
|
/>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
|
|
||||||
<Grid.Column width={10}>
|
<Grid.Column width={10}>
|
||||||
|
@ -182,7 +185,7 @@ export function Home(props) {
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
<Grid.Column>
|
<Grid.Column>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Header size='medium'>Portal</Header>
|
<Header size='medium'>Home</Header>
|
||||||
<p>Welcome to the Protospace member portal! Here you can view member info, join classes, and manage your membership.</p>
|
<p>Welcome to the Protospace member portal! Here you can view member info, join classes, and manage your membership.</p>
|
||||||
|
|
||||||
<Header size='medium'>Quick Links</Header>
|
<Header size='medium'>Quick Links</Header>
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom';
|
import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom';
|
||||||
import './light.css';
|
import './light.css';
|
||||||
import { Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Input, Menu, Message, Segment, Table } from 'semantic-ui-react';
|
import { Button, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Input, Menu, Message, Segment, Table } from 'semantic-ui-react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { requester } from './utils.js';
|
import { staticUrl, requester } from './utils.js';
|
||||||
import { NotFound, PleaseLogin } from './Misc.js';
|
import { NotFound, PleaseLogin } from './Misc.js';
|
||||||
|
|
||||||
export function Members(props) {
|
export function Members(props) {
|
||||||
const [members, setMembers] = useState(false);
|
const [members, setMembers] = useState(false);
|
||||||
const [search, setSearch] = useState({seq: 0, q: ''});
|
const searchDefault = {seq: 0, q: ''};
|
||||||
|
const [search, setSearch] = useState(searchDefault);
|
||||||
const { token } = props;
|
const { token } = props;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
requester('/search/', 'POST', token, search)
|
requester('/search/', 'POST', token, search)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (!members || res.seq > members.seq) {
|
if (!search.seq || res.seq > members.seq) {
|
||||||
setMembers(res);
|
setMembers(res);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -29,19 +30,28 @@ export function Members(props) {
|
||||||
|
|
||||||
<Input autoFocus focus icon='search'
|
<Input autoFocus focus icon='search'
|
||||||
placeholder='Search...'
|
placeholder='Search...'
|
||||||
value={search.v}
|
value={search.q}
|
||||||
onChange={(e, v) => setSearch({seq: e.timeStamp, q: v.value})}
|
onChange={(e, v) => setSearch({seq: e.timeStamp, q: v.value})}
|
||||||
aria-label='search products'
|
aria-label='search products'
|
||||||
|
style={{ marginRight: '0.5rem' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{search.q.length ?
|
||||||
|
<Button
|
||||||
|
content='Clear'
|
||||||
|
onClick={() => setSearch(searchDefault)}
|
||||||
|
/> : ''
|
||||||
|
}
|
||||||
|
|
||||||
<Header size='medium'>
|
<Header size='medium'>
|
||||||
{search.q.length ? 'Search Results' : 'Recently Vetted'}
|
{search.q.length ? 'Search Results' : 'Newest Vetted Members'}
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
{members ?
|
{members ?
|
||||||
<Table basic='very'>
|
<Table basic='very'>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
|
<Table.HeaderCell></Table.HeaderCell>
|
||||||
<Table.HeaderCell>Name</Table.HeaderCell>
|
<Table.HeaderCell>Name</Table.HeaderCell>
|
||||||
<Table.HeaderCell>Status</Table.HeaderCell>
|
<Table.HeaderCell>Status</Table.HeaderCell>
|
||||||
<Table.HeaderCell>Member Since</Table.HeaderCell>
|
<Table.HeaderCell>Member Since</Table.HeaderCell>
|
||||||
|
@ -52,6 +62,12 @@ export function Members(props) {
|
||||||
{members.results.length ?
|
{members.results.length ?
|
||||||
members.results.map((x, i) =>
|
members.results.map((x, i) =>
|
||||||
<Table.Row key={i}>
|
<Table.Row key={i}>
|
||||||
|
<Table.Cell>
|
||||||
|
<img
|
||||||
|
className='photo-small'
|
||||||
|
src={x.photo_small ? staticUrl + '/' + x.photo_small : 'nophoto.png'}
|
||||||
|
/>
|
||||||
|
</Table.Cell>
|
||||||
<Table.Cell>{x.preferred_name} {x.last_name}</Table.Cell>
|
<Table.Cell>{x.preferred_name} {x.last_name}</Table.Cell>
|
||||||
<Table.Cell>{x.status}</Table.Cell>
|
<Table.Cell>{x.status}</Table.Cell>
|
||||||
<Table.Cell>{x.current_start_date}</Table.Cell>
|
<Table.Cell>{x.current_start_date}</Table.Cell>
|
||||||
|
|
|
@ -29,3 +29,13 @@ body {
|
||||||
.topPadding {
|
.topPadding {
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.photo-small {
|
||||||
|
max-width: 80px;
|
||||||
|
max-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-medium {
|
||||||
|
max-width: 150px;
|
||||||
|
max-height: 1500px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
var siteUrl, apiUrl;
|
var siteUrl, apiUrl, staticUrl;
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
siteUrl = 'http://spaceport.dns.t0.vc';
|
siteUrl = 'http://spaceport.dns.t0.vc';
|
||||||
apiUrl = 'http://spaceport-api.dns.t0.vc';
|
apiUrl = 'http://spaceport-api.dns.t0.vc';
|
||||||
|
staticUrl = 'http://spaceport-static.dns.t0.vc';
|
||||||
} else {
|
} else {
|
||||||
siteUrl = 'https://' + window.location.hostname;
|
siteUrl = 'https://' + window.location.hostname;
|
||||||
apiUrl = 'https://api.' + window.location.hostname;
|
apiUrl = 'https://api.' + window.location.hostname;
|
||||||
|
staticUrl = 'https://static.' + window.location.hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { siteUrl, apiUrl, staticUrl };
|
||||||
|
|
||||||
export const requester = (route, method, token, data) => {
|
export const requester = (route, method, token, data) => {
|
||||||
let options = {headers: {}};
|
let options = {headers: {}};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user