Import old photos and link to members

This commit is contained in:
Tanner Collin 2020-01-11 06:27:30 +00:00
parent 5091ff2977
commit 946c70d776
9 changed files with 96 additions and 25 deletions

View File

@ -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)

View File

@ -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):

View File

@ -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]

View File

@ -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
)) ))

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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: {}};