diff --git a/apiserver/apiserver/api/models.py b/apiserver/apiserver/api/models.py index 7d6f465..5050ffc 100644 --- a/apiserver/apiserver/api/models.py +++ b/apiserver/apiserver/api/models.py @@ -21,6 +21,9 @@ class Member(models.Model): monthly_fees = models.IntegerField(default=55, blank=True, null=True) emergency_contact_name = models.CharField(max_length=64, 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): user = models.ForeignKey(User, related_name='transactions', blank=True, null=True, on_delete=models.SET_NULL) diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py index cead746..aa7da3a 100644 --- a/apiserver/apiserver/api/serializers.py +++ b/apiserver/apiserver/api/serializers.py @@ -2,11 +2,46 @@ from django.contrib.auth.models import User, Group from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_auth.registration.serializers import RegisterSerializer +from uuid import uuid4 +from PIL import Image from . import models, old_models #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 Meta: model = models.Training @@ -29,7 +64,7 @@ class OtherMemberSerializer(serializers.ModelSerializer): class Meta: 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 class MemberSerializer(serializers.ModelSerializer): diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py index 20fe8d1..c0a4cea 100644 --- a/apiserver/apiserver/api/views.py +++ b/apiserver/apiserver/api/views.py @@ -16,9 +16,6 @@ class AllowMetadata(permissions.BasePermission): search_strings = {} def gen_search_strings(): - import time - start = time.time() - for m in models.Member.objects.all(): string = '{} {}'.format( m.preferred_name, @@ -26,9 +23,6 @@ def gen_search_strings(): ).lower() search_strings[string] = m.id - print('Generated search strings in {} s'.format(time.time() - start)) -gen_search_strings() - class SearchViewSet(viewsets.ViewSet): permission_classes = [AllowMetadata | permissions.IsAuthenticated] serializer_class = serializers.OtherMemberSerializer @@ -60,6 +54,7 @@ class SearchViewSet(viewsets.ViewSet): queryset = result_objects else: + gen_search_strings() queryset = queryset.order_by('-vetted_date')[:NUM_SEARCH_RESULTS] return queryset @@ -93,13 +88,6 @@ class MemberViewSet(viewsets.ModelViewSet): else: 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): permission_classes = [AllowMetadata | permissions.IsAuthenticated] diff --git a/apiserver/import_old_portal.py b/apiserver/import_old_portal.py index b60d227..65eeaaa 100755 --- a/apiserver/import_old_portal.py +++ b/apiserver/import_old_portal.py @@ -3,6 +3,7 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'apiserver.settings' django.setup() from apiserver.api import models, old_models +from apiserver.api.serializers import process_image MEMBER_FIELDS = [ 'id', @@ -67,6 +68,10 @@ TRAINING_FIELDS = [ 'paid_date', ] +photo_folders = os.listdir('old_photos') +print('Found {} member photo folders'.format(len(photo_folders))) + + print('Deleting all members...') models.Member.objects.all().delete() print('Importing old members...') @@ -78,7 +83,14 @@ for o in old: for f in MEMBER_FIELDS: 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( o.id, o.first_name, o.last_name )) diff --git a/webclient/public/nophoto.png b/webclient/public/nophoto.png new file mode 100644 index 0000000..e7bdbef Binary files /dev/null and b/webclient/public/nophoto.png differ diff --git a/webclient/src/Home.js b/webclient/src/Home.js index 167ab1d..daa0941 100644 --- a/webclient/src/Home.js +++ b/webclient/src/Home.js @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom'; import './light.css'; 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'; function DetailsForm(props) { @@ -89,7 +89,10 @@ function MemberInfo(props) {
- + @@ -182,7 +185,7 @@ export function Home(props) { -
Portal
+
Home

Welcome to the Protospace member portal! Here you can view member info, join classes, and manage your membership.

Quick Links
diff --git a/webclient/src/Members.js b/webclient/src/Members.js index 6884030..59b6ea8 100644 --- a/webclient/src/Members.js +++ b/webclient/src/Members.js @@ -1,20 +1,21 @@ import React, { useState, useEffect } from 'react'; import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom'; 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 { requester } from './utils.js'; +import { staticUrl, requester } from './utils.js'; import { NotFound, PleaseLogin } from './Misc.js'; export function Members(props) { 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; useEffect(() => { requester('/search/', 'POST', token, search) .then(res => { - if (!members || res.seq > members.seq) { + if (!search.seq || res.seq > members.seq) { setMembers(res); } }) @@ -29,19 +30,28 @@ export function Members(props) { setSearch({seq: e.timeStamp, q: v.value})} aria-label='search products' + style={{ marginRight: '0.5rem' }} /> + {search.q.length ? +