diff --git a/apiserver/.gitignore b/apiserver/.gitignore index 5b448cc..cb5a5f9 100644 --- a/apiserver/.gitignore +++ b/apiserver/.gitignore @@ -104,4 +104,6 @@ ENV/ # DB db.sqlite3 +old_portal.sqlite3 +old_models.py migrations/ diff --git a/apiserver/apiserver/api/models.py b/apiserver/apiserver/api/models.py index 71a8362..1481f54 100644 --- a/apiserver/apiserver/api/models.py +++ b/apiserver/apiserver/api/models.py @@ -1,3 +1,20 @@ from django.db import models +from django.contrib.auth.models import User -# Create your models here. +from . import old_models + +class Member(models.Model): + user = models.OneToOneField(User, on_delete=models.PROTECT) + first_name = models.CharField(max_length=32) + last_name = models.CharField(max_length=32) + old_member_id = models.IntegerField(null=True, blank=True) + + set_details = models.BooleanField(default=False) + preferred_name = models.CharField(max_length=32, blank=True) + phone = models.CharField(max_length=32, blank=True) + current_start_date = models.DateField(blank=True, null=True) + application_date = models.DateField(blank=True, null=True) + vetted_date = models.DateField(blank=True, null=True) + monthly_fees = models.IntegerField(blank=True, null=True) + emergency_contact_name = models.CharField(max_length=64, blank=True) + emergency_contact_phone = models.CharField(max_length=32, blank=True) diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py index c1a712a..6d0dc52 100644 --- a/apiserver/apiserver/api/serializers.py +++ b/apiserver/apiserver/api/serializers.py @@ -1,7 +1,70 @@ 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 . import models, old_models + +GRAB_FIELDS = [ + 'preferred_name', + 'phone', + 'current_start_date', + 'application_date', + 'vetted_date', + 'monthly_fees', + 'emergency_contact_name', + 'emergency_contact_phone', +] + +#custom_error = lambda x: ValidationError(dict(non_field_errors=x)) class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['id', 'username', 'email', 'groups'] + fields = ['id', 'username', 'email', 'member'] + depth = 1 + + +class MemberSerializer(serializers.ModelSerializer): + class Meta: + model = models.Member + fields = '__all__' + read_only_fields = ['user', 'application_date', 'current_start_date', 'vetted_date', 'monthly_fees', 'old_member_id'] + +class AdminMemberSerializer(serializers.ModelSerializer): + class Meta: + model = models.Member + fields = '__all__' + read_only_fields = ['id', 'user'] + + +class RegistrationSerializer(RegisterSerializer): + first_name = serializers.CharField(max_length=32) + last_name = serializers.CharField(max_length=32) + existing_member = serializers.ChoiceField(['true', 'false']) + + def custom_signup(self, request, user): + data = request.data + old_member_id = None + old_member_fields = dict(preferred_name=data['first_name']) + + if data['existing_member'] == 'true': + old_members = old_models.Members.objects.using('old_portal') + try: + old_member = old_members.get(email=data['email']) + except old_models.Members.DoesNotExist: + user.delete() + raise ValidationError(dict(email='Unable to find in old database.')) + + old_member_id = old_member.id + + for f in GRAB_FIELDS: + old_member_fields[f] = old_member.__dict__.get(f, None) + + models.Member.objects.create( + user=user, + first_name=data['first_name'], + last_name=data['last_name'], + old_member_id=old_member_id, + **old_member_fields + ) diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py index a7befe8..0fbf760 100644 --- a/apiserver/apiserver/api/views.py +++ b/apiserver/apiserver/api/views.py @@ -1,8 +1,55 @@ from django.contrib.auth.models import User, Group -from rest_framework import viewsets +from rest_framework import viewsets, views, permissions +from rest_framework.response import Response +from rest_auth.registration.views import RegisterView from . import models, serializers +class AllowMetadata(permissions.BasePermission): + def has_permission(self, request, view): + return request.method in ['OPTIONS', 'HEAD'] + + class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all().order_by('-date_joined') serializer_class = serializers.UserSerializer + + +class MemberViewSet(viewsets.ModelViewSet): + permission_classes = [AllowMetadata | permissions.IsAuthenticated] + http_method_names = ['options', 'head', 'get', 'put', 'patch'] + + def get_queryset(self): + objects = models.Member.objects.all() + if self.request.user.is_staff: + return objects.order_by('id') + else: + return objects.filter(user=self.request.user) + + def get_serializer_class(self): + if self.request.user.is_staff: + return serializers.AdminMemberSerializer + else: + return serializers.MemberSerializer + + +class MyUserView(views.APIView): + permission_classes = [AllowMetadata | permissions.IsAuthenticated] + + def get(self, request): + serializer = serializers.UserSerializer(request.user) + return Response(serializer.data) + + +class RegistrationViewSet(RegisterView): + serializer_class = serializers.RegistrationSerializer + + #def create(self, request): + # data = request.data.copy() + # data['username'] = '{}.{}'.format( + # data['first_name'], + # data['last_name'] + # ).lower() + # request._full_data = data + # return super().create(request) + diff --git a/apiserver/apiserver/settings.py b/apiserver/apiserver/settings.py index 2dd800b..9c71c11 100644 --- a/apiserver/apiserver/settings.py +++ b/apiserver/apiserver/settings.py @@ -53,6 +53,7 @@ INSTALLED_APPS = [ 'rest_auth', 'allauth', 'allauth.account', + 'allauth.socialaccount', # to support user deletion 'rest_auth.registration', ] @@ -94,6 +95,10 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + }, + 'old_portal': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'old_portal.sqlite3'), } } @@ -209,3 +214,6 @@ LOGGING = { } SITE_ID = 1 +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_EMAIL_VERIFICATION = 'none' +ACCOUNT_USERNAME_MIN_LENGTH = 3 diff --git a/apiserver/apiserver/urls.py b/apiserver/apiserver/urls.py index 617523c..ba87740 100644 --- a/apiserver/apiserver/urls.py +++ b/apiserver/apiserver/urls.py @@ -7,11 +7,15 @@ from .api import views router = routers.DefaultRouter() router.register(r'users', views.UserViewSet) +router.register(r'members', views.MemberViewSet, basename='member') +#router.register(r'me', views.FullMemberView, basename='fullmember') +#router.register(r'registration', views.RegistrationViewSet, basename='register') urlpatterns = [ path('', include(router.urls)), path('admin/', admin.site.urls), path('api-auth/', include('rest_framework.urls')), url(r'^rest-auth/', include('rest_auth.urls')), - url(r'^rest-auth/registration/', include('rest_auth.registration.urls')) + url(r'^registration/', views.RegistrationViewSet.as_view(), name='rest_name_register'), + url(r'^me/', views.MyUserView.as_view(), name='fullmember'), ] diff --git a/apiserver/gen_old_models.sh b/apiserver/gen_old_models.sh new file mode 100755 index 0000000..fb42a00 --- /dev/null +++ b/apiserver/gen_old_models.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +python manage.py inspectdb --database old_portal | sed 's/CharField/TextField/g' > apiserver/api/old_models.py diff --git a/webclient/.gitignore b/webclient/.gitignore index 4d29575..abd67fb 100644 --- a/webclient/.gitignore +++ b/webclient/.gitignore @@ -21,3 +21,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# Editor +*.swp +*.swo diff --git a/webclient/public/.index.html.swp b/webclient/public/.index.html.swp deleted file mode 100644 index 6b3fce6..0000000 Binary files a/webclient/public/.index.html.swp and /dev/null differ diff --git a/webclient/src/.App.js.swp b/webclient/src/.App.js.swp deleted file mode 100644 index 5160013..0000000 Binary files a/webclient/src/.App.js.swp and /dev/null differ diff --git a/webclient/src/.light.css.swp b/webclient/src/.light.css.swp deleted file mode 100644 index a783329..0000000 Binary files a/webclient/src/.light.css.swp and /dev/null differ diff --git a/webclient/src/.utils.js.swp b/webclient/src/.utils.js.swp deleted file mode 100644 index 34c071c..0000000 Binary files a/webclient/src/.utils.js.swp and /dev/null differ diff --git a/webclient/src/App.js b/webclient/src/App.js index a4f3e0a..cd8a336 100644 --- a/webclient/src/App.js +++ b/webclient/src/App.js @@ -1,25 +1,31 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import './light.css'; import Logo from './logo.svg'; -import { Container, Divider, Form, Grid, Header, Menu, Message } from 'semantic-ui-react'; +import { Container, Divider, Form, Grid, Header, Icon, Menu, Message } from 'semantic-ui-react'; import { requester } from './utils.js'; -function LoginForm() { - const [input, setInput] = useState({}) - const [error, setError] = useState({}) +function LoginForm(props) { + const [input, setInput] = useState({}); + const [error, setError] = useState({}); + const [loading, setLoading] = useState(false); - const handleChange = (e) => setInput({ + const handleValues = (e, v) => setInput({ ...input, - [e.currentTarget.name]: e.currentTarget.value + [v.name]: v.value }); + const handleChange = (e) => handleValues(e, e.currentTarget); + const handleSubmit = (e) => { - requester('/rest-auth/login/', 'POST', input) + setLoading(true); + requester('/rest-auth/login/', 'POST', '', input) .then(res => { console.log(res); setError({}); + props.setTokenCache(res.key); }) .catch(err => { + setLoading(false); console.log(err); setError(err.data); }); @@ -27,6 +33,7 @@ function LoginForm() { return (
+
Login to Spaceport
- + Login ); } -function SignupForm() { - const [input, setInput] = useState({}) +function SignupForm(props) { + const [input, setInput] = useState({}); + const [error, setError] = useState({}); + const [loading, setLoading] = useState(false); - const handleChange = (e) => setInput({ + const handleValues = (e, v) => setInput({ ...input, - [e.currentTarget.name]: e.currentTarget.value + [v.name]: v.value }); + const handleChange = (e) => handleValues(e, e.currentTarget); + + const genUsername = () => ( + input.first_name && input.last_name ? + (input.first_name + '.' + input.last_name).toLowerCase() + : + '' + ); + const handleSubmit = (e) => { - console.log(input); - } + setLoading(true); + input.username = genUsername(); + requester('/registration/', 'POST', '', input) + .then(res => { + console.log(res); + setError({}); + props.setTokenCache(res.key); + }) + .catch(err => { + setLoading(false); + console.log(err); + setError(err.data); + }); + }; return (
+
Sign Up
+ + + + + + + + @@ -80,30 +145,133 @@ function SignupForm() { name='password1' type='password' onChange={handleChange} + error={error.password1} /> + + + Sign Up + + + ); +} + +function DetailsForm(props) { + const member = props.user.member; + const [input, setInput] = useState({ + preferred_name: member.preferred_name, + phone: member.phone, + emergency_contact_name: member.emergency_contact_name, + emergency_contact_phone: member.emergency_contact_phone, + set_details: true, + }); + const [error, setError] = useState({}); + const [loading, setLoading] = useState(false); + + const handleValues = (e, v) => setInput({ + ...input, + [v.name]: v.value + }); + + const handleChange = (e) => handleValues(e, e.currentTarget); + + const handleSubmit = (e) => { + setLoading(true); + requester('/members/' + member.id + '/', 'PATCH', props.token, input) + .then(res => { + console.log(res); + setError({}); + props.setUserCache({...props.user, member: res}); + }) + .catch(err => { + setLoading(false); + console.log(err); + setError(err.data); + }); + }; + + return ( +
+
Enter Details
+ - Sign Up + + + + + Submit + ); } function App() { + const [token, setToken] = useState(localStorage.getItem('token', '')); + const [user, setUser] = useState(JSON.parse(localStorage.getItem('user', 'false'))); + + const setTokenCache = (x) => { + setToken(x); + localStorage.setItem('token', x); + } + + const setUserCache = (x) => { + setUser(x); + localStorage.setItem('user', JSON.stringify(x)); + } + + useEffect(() => { + requester('/me/', 'GET', token) + .then(res => { + console.log(res); + setUserCache(res); + }) + .catch(err => { + console.log(err); + setUser(false); + }); + }, [token]); + + const logout = () => { + setTokenCache(''); + setUserCache(false); + } + return (
-
+
-
+
@@ -117,20 +285,33 @@ function App() { + + {user && + + } -
Login to Spaceport
- - + {user ? + user.member.set_details ? +

yay welcome {user.member.first_name}

+ : + + : +
+ - Or + Or -
Sign Up
- + +
+ }

two

diff --git a/webclient/src/light.css b/webclient/src/light.css index 3ca0042..c5bf6b4 100644 --- a/webclient/src/light.css +++ b/webclient/src/light.css @@ -1,5 +1,11 @@ +.header { + padding-top: 1.5rem; + margin-bottom: 1.5rem; +} + .header .logo { max-width: 100%; + height: 2rem; display: block; - margin: 1.5rem auto; + margin: auto; } diff --git a/webclient/src/utils.js b/webclient/src/utils.js index 7df3474..c3518f7 100644 --- a/webclient/src/utils.js +++ b/webclient/src/utils.js @@ -8,23 +8,28 @@ if (process.env.NODE_ENV !== 'production') { apiUrl = 'https://api.' + window.location.hostname; } -export const requester = (route, method, data) => { - var options; +export const requester = (route, method, token, data) => { + let options = {headers: {}}; + + if (token) { + options.headers.Authorization = 'Token ' + token; + } if (method == 'GET') { - options = {}; - } else if (method == 'POST') { + // pass + } else if (['POST', 'PUT', 'PATCH'].includes(method)) { const formData = new FormData(); Object.keys(data).forEach(key => formData.append(key, data[key]) ); options = { - method: 'POST', + ...options, + method: method, body: formData, }; } else { - return 'Method not supported'; + throw new Error('Method not supported'); } const customError = (data) => { @@ -44,11 +49,13 @@ export const requester = (route, method, data) => { const code = error.data.status; if (code == 413) { throw customError({non_field_errors: ['File too big']}); - } else if (code == 400) { + } else if (code >= 400 && code < 500) { return error.data.json() .then(result => { throw customError(result); }); + } else if (code >= 500 && code < 600) { + throw customError({non_field_errors: ['Server Error']}); } else { throw customError({non_field_errors: ['Network Error']}); }