From 4af72a43e5d2b96a821d1dfb0e77c51a2b9fc823 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Wed, 17 Nov 2021 04:50:39 +0000
Subject: [PATCH 01/19] Load more search results, maintain scroll position
---
apiserver/apiserver/api/utils.py | 4 +
apiserver/apiserver/api/views.py | 97 +++++++++++-----------
webclient/src/Members.js | 138 +++++++++++++++++++++----------
3 files changed, 148 insertions(+), 91 deletions(-)
diff --git a/apiserver/apiserver/api/utils.py b/apiserver/apiserver/api/utils.py
index 55b3cce..d802e20 100644
--- a/apiserver/apiserver/api/utils.py
+++ b/apiserver/apiserver/api/utils.py
@@ -146,6 +146,8 @@ def gen_search_strings():
'''
Generate a cache dict of names to member ids for rapid string matching
'''
+ start = time.time()
+
search_strings = {}
for m in models.Member.objects.order_by('-expire_date'):
string = '{} {} | {} {}'.format(
@@ -165,6 +167,8 @@ def gen_search_strings():
search_strings[string] = m.id
cache.set('search_strings', search_strings)
+ logger.info('Generated search strings in %s s.', time.time() - start)
+
LARGE_SIZE = 1080
MEDIUM_SIZE = 220
diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py
index 13c0d79..c0ddd0b 100644
--- a/apiserver/apiserver/api/views.py
+++ b/apiserver/apiserver/api/views.py
@@ -4,7 +4,7 @@ logger = logging.getLogger(__name__)
from django.contrib.auth.models import User, Group
from django.shortcuts import get_object_or_404, redirect
from django.db import transaction
-from django.db.models import Max, F
+from django.db.models import Max, F, Count
from django.db.utils import OperationalError
from django.http import HttpResponse, Http404, FileResponse
from django.core.files.base import File
@@ -43,7 +43,7 @@ Create = mixins.CreateModelMixin
Update = mixins.UpdateModelMixin
Destroy = mixins.DestroyModelMixin
-NUM_SEARCH_RESULTS = 20
+NUM_RESULTS = 100
class SearchViewSet(Base, Retrieve):
@@ -79,54 +79,57 @@ class SearchViewSet(Base, Retrieve):
if len(results) == 0 and len(search) >= 3 and '@' not in search:
# then get fuzzy matches, but not for emails
- fuzzy_results = process.extract(search, choices, limit=NUM_SEARCH_RESULTS, scorer=fuzz.token_set_ratio)
+ fuzzy_results = process.extract(search, choices, limit=20, scorer=fuzz.token_set_ratio)
results += [x[0] for x in fuzzy_results]
# remove dupes, truncate list
- results = list(OrderedDict.fromkeys(results))[:NUM_SEARCH_RESULTS]
+ results = list(OrderedDict.fromkeys(results))[:20]
result_ids = [search_strings[x] for x in results]
result_objects = [queryset.get(id=x) for x in result_ids]
queryset = result_objects
logging.info('Search for: {}, results: {}'.format(search, len(queryset)))
- elif self.action == 'create' and sort == 'recently_vetted':
- utils.gen_search_strings() # update cache
- queryset = queryset.order_by('-vetted_date')
- elif self.action == 'create' and sort == 'newest_active':
- queryset = queryset.filter(paused_date__isnull=True)
- queryset = queryset.order_by('-application_date')
- elif self.action == 'create' and sort == 'newest_overall':
- queryset = queryset.order_by('-application_date')
- elif self.action == 'create' and sort == 'oldest_active':
- queryset = queryset.filter(paused_date__isnull=True)
- queryset = queryset.order_by('application_date')
- elif self.action == 'create' and sort == 'oldest_overall':
- queryset = queryset.filter(application_date__isnull=False)
- queryset = queryset.order_by('application_date')
- elif self.action == 'create' and sort == 'recently_inactive':
- queryset = queryset.filter(paused_date__isnull=False)
- queryset = queryset.order_by('-paused_date')
- elif self.action == 'create' and sort == 'is_director':
- queryset = queryset.filter(is_director=True)
- queryset = queryset.order_by('application_date')
- elif self.action == 'create' and sort == 'is_instructor':
- queryset = queryset.filter(is_instructor=True)
- queryset = queryset.order_by('application_date')
- elif self.action == 'create' and sort == 'due':
- queryset = queryset.filter(status='Due')
- queryset = queryset.order_by('expire_date')
- elif self.action == 'create' and sort == 'overdue':
- queryset = queryset.filter(status='Overdue')
- queryset = queryset.order_by('expire_date')
- elif self.action == 'create' and sort == 'last_scanned':
- if self.request.user.member.allow_last_scanned:
- queryset = queryset.filter(allow_last_scanned=True)
- queryset = queryset.order_by('-user__cards__last_seen')
- else:
+ elif self.action == 'create':
+ if sort == 'recently_vetted':
+ queryset = queryset.filter(vetted_date__isnull=False)
+ queryset = queryset.order_by('-vetted_date')
+ elif sort == 'newest_active':
+ queryset = queryset.filter(paused_date__isnull=True)
+ queryset = queryset.order_by('-application_date')
+ elif sort == 'newest_overall':
+ queryset = queryset.order_by('-application_date')
+ elif sort == 'oldest_active':
+ queryset = queryset.filter(paused_date__isnull=True)
+ queryset = queryset.order_by('application_date')
+ elif sort == 'oldest_overall':
+ queryset = queryset.filter(application_date__isnull=False)
+ queryset = queryset.order_by('application_date')
+ elif sort == 'recently_inactive':
+ queryset = queryset.filter(paused_date__isnull=False)
+ queryset = queryset.order_by('-paused_date')
+ elif sort == 'is_director':
+ queryset = queryset.filter(is_director=True)
+ queryset = queryset.order_by('application_date')
+ elif sort == 'is_instructor':
+ queryset = queryset.filter(is_instructor=True)
+ queryset = queryset.order_by('application_date')
+ elif sort == 'due':
+ queryset = queryset.filter(status='Due')
+ queryset = queryset.order_by('expire_date')
+ elif sort == 'overdue':
+ queryset = queryset.filter(status='Overdue')
+ queryset = queryset.order_by('expire_date')
+ elif sort == 'last_scanned':
+ if self.request.user.member.allow_last_scanned:
+ queryset = queryset.filter(allow_last_scanned=True)
+ queryset = queryset.order_by('-user__cards__last_seen')
+ else:
+ queryset = []
+ elif sort == 'everyone':
+ queryset = queryset.annotate(Count('user__transactions')).order_by('-user__transactions__count')
+ elif sort == 'best_looking':
queryset = []
- elif self.action == 'create' and sort == 'best_looking':
- queryset = []
return queryset
@@ -139,19 +142,19 @@ class SearchViewSet(Base, Retrieve):
seq = 0
search = self.request.data.get('q', '').lower()
- if search:
- num_results = NUM_SEARCH_RESULTS
- else:
- num_results = 100
+ page = self.request.data.get('page', 0)
+ queryset = self.get_queryset()
+ total = len(queryset)
- queryset = self.get_queryset()[:num_results]
+ start = int(page) * NUM_RESULTS - 80
+ queryset = queryset[max(start,0):start+NUM_RESULTS]
if self.request.user.member.vetted_date:
serializer = serializers.VettedSearchSerializer(queryset, many=True)
else:
serializer = serializers.SearchSerializer(queryset, many=True)
- return Response({'seq': seq, 'results': serializer.data})
+ return Response({'seq': seq, 'results': serializer.data, 'total': total})
class MemberViewSet(Base, Retrieve, Update):
@@ -168,11 +171,13 @@ class MemberViewSet(Base, Retrieve, Update):
member = serializer.save()
utils.tally_membership_months(member)
utils.gen_member_forms(member)
+ utils.gen_search_strings()
def perform_update(self, serializer):
member = serializer.save()
utils.tally_membership_months(member)
utils.gen_member_forms(member)
+ utils.gen_search_strings()
@action(detail=True, methods=['post'])
def pause(self, request, pk=None):
diff --git a/webclient/src/Members.js b/webclient/src/Members.js
index 02f4328..e4086aa 100644
--- a/webclient/src/Members.js
+++ b/webclient/src/Members.js
@@ -16,11 +16,12 @@ const memberSorts = {
//newest_overall: 'Newest Overall',
oldest_active: 'Oldest',
//oldest_overall: 'Oldest Overall',
- recently_inactive: 'Inactive',
+ recently_inactive: 'Recently Inactive',
is_director: 'Directors',
is_instructor: 'Instructors',
due: 'Due',
overdue: 'Overdue',
+ everyone: 'Everyone',
};
export function MembersDropdown(props) {
@@ -65,28 +66,94 @@ export function MembersDropdown(props) {
);
};
-let numShowCache = 20;
+let responseCache = false;
+let pageCache = 0;
+let sortCache = '';
+let searchCache = '';
+
+const loadMoreStrings = [
+ 'Load More',
+ 'Load EVEN More',
+ 'Load WAY More',
+ 'Why did you stop? LOAD MORE!',
+ 'GIVE ME MORE NAMES!!',
+ 'Shower me with names, baby',
+ 'I don\'t care about the poor server, MORE NAMES!',
+ 'Names make me hotter than two rats in a wool sock',
+ 'Holy shit, I can\'t get enough names',
+ 'I don\'t have anything better to do than LOAD NAMES!',
+ 'I need names because I love N̶a̸M̸E̵S̴ it\'s not to late to stop but I can\'t because it feels so good god help me',
+ 'The One who loads the names will liquify the NERVES of the sentient whilst I o̴̭̐b̴̙̾s̷̺͝ē̶̟r̷̦̓v̸͚̐ę̸̈́ ̷̞̒t̸͘ͅh̴͂͜e̵̜̕i̶̾͜r̷̃͜ ̵̹͊Ḷ̷͝Ȍ̸͚Ä̶̘́D̴̰́I̸̧̚N̵͖̎G̷̣͒',
+ 'The Song of Names will will e̶̟̤͋x̷̜̀͘͜t̴̳̀i̸̪͑̇n̷̘̍g̵̥̗̓ṳ̴̑̈́i̷͚̿s̸̨̪̓ḣ̶̡̓ ̷̲͊ṫ̴̫h̸̙͕͗ḛ̸̡̃̈́ ̷̘̫̉̏v̸̧̟͗̕o̴͕̾͜i̷̢͛̿ͅc̴͕̥̈́̂ȅ̵͕s̶̹͋̀ ̶̰́͜͠ǒ̷̰̯f̵̛̥̊ ̸̟̟̒͝m̸̯̀̂o̶̝͛̌͜r̸̞̀ṫ̴̥͗ä̶̢́l̶̯̄͘ ̵̫̈́m̷̦̑̂ą̶͕͝ṋ̴̎͝ from the sphere I can see it can you see it it is beautiful',
+ 'The final suffering of T̷̯̂͝H̴̰̏̉Ḛ̸̀̓ ̷̟̒ͅN̷̠̾Ą̵̟̈́M̶̡̾͝E̸̥̟̐͐S̸̖̍ are lies all is lost the pony he come h̷̲̺͂̾͒̔͝ḙ̶̻͒͠ ̷̙̘͈̬̰̽̽̈́̒͘c̵͎̺̞̰͝ơ̷͚̱̺̰̺͐̏͑͠m̴̖̰̓̈͝ĕ̷̜s̶̛̹̤̦͉̓͝ the í̵̠̞̙̦̱̠̅̊͒̌͊̓͠͠c̴̻̺̙͕̲͚͔̩̥͑ḩ̷̦̰̠̯̳̖̘́̉̾̾͠o̴͈̯̟̣̲͙̦̖̖͍̞̞̻̎͐̊͊̇͋̒͛̅͆̌͂̈̕r̷̡̝̲̜͇͉̣̹̖͕̻̐̑̉̋͋̉͒͋̍́̒͐͐͘ͅ ̵̳̖͕̩̝̮͈̻̣̤͎̟͓̜̄̿̓̈́p̴̰̝͓̣͍̫̞͓̑͌͊͑̓̂̽͑͝e̶̛̪̜̐̋́̆͊͌̋̄́͘r̶̫̬͈͌̔̽m̶̛̱̣͍͌̈́͋̾̈̀͑̽̋̏̊͋͝ę̶̋̀̈̃͠ą̵̡̣̫̮͙͈͚̞̰̠̥͇̣̽̿̉́̔̒͌̓͌̂̌̕͜͠t̷̯͚̭̮̠̐͋͆́͛̿́̏̆̚ě̶̢̨̩̞ş̸̢͍̱̻͕̪̗̻͖͇̱̳̽̈́̚͠ ̴͉̝̖̤͚̖̩̻̪̒ͅà̸̙̥̩̠̝̪̰͋́̊̓͌́͒̕͝ĺ̵̖̖͚̱͎̤̟̲̺͎͑͋̐̈́̓͂͆̅̈́̎̆̋̇l̸̢̧̟͉̞͇̱͉̙͇͊̏͐͠ͅ',
+];
export function Members(props) {
- const history = useHistory();
- const qs = useLocation().search;
- const params = new URLSearchParams(qs);
- const sort = params.get('sort') || 'recently_vetted';
- const search = params.get('q') || '';
-
- const [response, setResponse] = useState(false);
- const [numShow, setNumShow] = useState(numShowCache);
+ const [response, setResponse] = useState(responseCache);
+ const [loading, setLoading] = useState(false);
+ const [page, setPage] = useState(pageCache);
+ const [sort, setSort] = useState(sortCache);
+ const [search, setSearch] = useState(searchCache);
const [controller, setController] = useState(false);
const { token, user } = props;
- const doSearch = (q) => {
- console.log('doing search', q);
- if (q.length) {
- const qs = queryString.stringify({ 'q': q });
- history.replace('/members?' + qs);
+ const makeRequest = ({loadPage, q, sort_key}) => {
+ let pageNum = 0;
+ if (loadPage) {
+ pageNum = page + 1;
+ setPage(pageNum);
+ pageCache = pageNum;
} else {
setResponse(false);
- history.replace('/members');
+ setPage(0);
+ pageCache = 0;
+ }
+
+ if (controller) {
+ controller.abort();
+ }
+ const ctl = new AbortController();
+ setController(ctl);
+ const signal = ctl.signal;
+
+ const data = {page: pageNum};
+ if (q) data.q = q;
+ if (sort_key) data.sort = sort_key;
+
+ requester('/search/', 'POST', token, data, signal)
+ .then(res => {
+ const r = loadPage ? {...response, results: [...response.results, ...res.results]} : res;
+ setResponse(r);
+ responseCache = r;
+ setLoading(false);
+ })
+ .catch(err => {
+ console.log('Aborted.');
+ });
+ }
+
+ const loadMore = () => {
+ setLoading(true);
+ makeRequest({loadPage: true, q: search, sort_key: sort});
+ };
+
+ const doSort = (sort_key) => {
+ setSort(sort_key);
+ sortCache = sort_key;
+ setSearch('');
+ searchCache = '';
+ makeRequest({loadPage: false, sort_key: sort_key});
+ };
+
+ const doSearch = (q) => {
+ if (q) {
+ setSearch(q);
+ searchCache = q;
+ setSort('');
+ sortCache = '';
+ makeRequest({loadPage: false, q: q});
+ } else {
+ doSort('recently_vetted');
}
};
@@ -96,26 +163,10 @@ export function Members(props) {
};
useEffect(() => {
- if (controller) {
- controller.abort();
+ if (!responseCache) {
+ doSort('recently_vetted');
}
- const ctl = new AbortController();
- setController(ctl);
- const signal = ctl.signal;
-
- const data = {q: search, sort: sort};
- requester('/search/', 'POST', token, data, signal)
- .then(res => {
- setResponse(res);
- })
- .catch(err => {
- ;
- });
- }, [search, sort]);
-
- useEffect(() => {
- setResponse(false);
- }, [sort]);
+ }, []);
return (
@@ -144,7 +195,7 @@ export function Members(props) {
Sort by{' '}
{Object.entries(memberSorts).map((x, i) =>
<>
- {x[1]}
+ doSort(x[0])}>{x[1]}
{i < Object.keys(memberSorts).length - 1 && ', '}
>
)}.
@@ -164,9 +215,11 @@ export function Members(props) {
{response ?
<>
+ {response.total} results:
+
- {response.results.length ?
- response.results.slice(0, numShow).map((x, i) =>
+ {!!response.results.length &&
+ response.results.map((x, i) =>
-
{i+1}
@@ -181,16 +234,11 @@ export function Members(props) {
)
- :
- No Results
}
- {response.results.length > 20 && numShow !== 100 ?
-
+
+ Precix availability: {getTrackStat('CNC-PRECIX')}
+
+ Last use:
+ {getTrackLast('CNC-PRECIX')}
+ {getTrackAgo('CNC-PRECIX')}
+ by {getTrackName('CNC-PRECIX')}
+
+
+ } trigger={[more]} />
+
+
{user && Alarm status: {alarmStat()}{doorOpenStat()}
}
From 02f080df774f0b4826ee05cab8d6aa422e42eaf3 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Thu, 25 Nov 2021 06:02:58 +0000
Subject: [PATCH 03/19] Handle username generation better
---
apiserver/apiserver/api/serializers.py | 2 +-
apiserver/apiserver/api/utils.py | 12 ++++++------
webclient/src/LoginSignup.js | 20 ++++++++++++++------
3 files changed, 21 insertions(+), 13 deletions(-)
diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py
index 81af3cd..5a0f28c 100644
--- a/apiserver/apiserver/api/serializers.py
+++ b/apiserver/apiserver/api/serializers.py
@@ -570,7 +570,7 @@ class MyRegisterSerializer(RegisterSerializer):
if re.search(r'[^a-z.]', username):
raise ValidationError('Invalid characters.')
if '..' in username:
- raise ValidationError('Can\'t have double periods.')
+ raise ValidationError('Can\'t have double periods. Remove spaces.')
if username.startswith('.') or username.endswith('.'):
raise ValidationError('Can\'t start or end with periods.')
return super().validate_username(username)
diff --git a/apiserver/apiserver/api/utils.py b/apiserver/apiserver/api/utils.py
index d802e20..2640a81 100644
--- a/apiserver/apiserver/api/utils.py
+++ b/apiserver/apiserver/api/utils.py
@@ -340,9 +340,9 @@ def link_old_member(data, user):
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Linking old member data...')
member.user = user
- member.first_name = data['first_name'].title()
- member.last_name = data['last_name'].title()
- member.preferred_name = data['first_name'].title()
+ member.first_name = data['first_name'].title().strip()
+ member.last_name = data['last_name'].title().strip()
+ member.preferred_name = data['first_name'].title().strip()
member.save()
models.Transaction.objects.filter(member_id=member.id).update(user=user)
@@ -381,9 +381,9 @@ def create_new_member(data, user):
models.Member.objects.create(
user=user,
- first_name=data['first_name'].title(),
- last_name=data['last_name'].title(),
- preferred_name=data['first_name'].title(),
+ first_name=data['first_name'].title().strip(),
+ last_name=data['last_name'].title().strip(),
+ preferred_name=data['first_name'].title().strip(),
)
def register_user(data, user):
diff --git a/webclient/src/LoginSignup.js b/webclient/src/LoginSignup.js
index 05ede0c..84d7b00 100644
--- a/webclient/src/LoginSignup.js
+++ b/webclient/src/LoginSignup.js
@@ -79,12 +79,20 @@ export function SignupForm(props) {
const handleValues = (e, v) => setInput({ ...input, [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().replace(/ /g, '.')
- :
- ''
- );
+ const genUsername = () => {
+ if (input.first_name && input.last_name) {
+ let first_name = input.first_name.trim();
+ let last_name = input.last_name.trim();
+ first_name = first_name.replace(/[^a-z- ]+/g, '');
+ last_name = last_name.replace(/[^a-z- ]+/g, '');
+ first_name = first_name.replace(/[ -]/g, '.');
+ last_name = last_name.replace(/[ -]/g, '.');
+ const username = first_name + '.' + last_name;
+ return username.toLowerCase();
+ } else {
+ return '';
+ }
+ };
const handleSubmit = (e) => {
if (loading) return;
From bebbdd5762ad2bd08ccdc7e9f81c84a52c7e6b08 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Fri, 26 Nov 2021 00:47:56 +0000
Subject: [PATCH 04/19] Freeze requirements
---
apiserver/requirements.txt | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/apiserver/requirements.txt b/apiserver/requirements.txt
index b2839bf..6c7cdd9 100644
--- a/apiserver/requirements.txt
+++ b/apiserver/requirements.txt
@@ -6,6 +6,7 @@ bleach==3.3.0
certifi==2019.11.28
cffi==1.13.2
chardet==3.0.4
+commonmark==0.9.1
defusedxml==0.6.0
Django==3.1.13
django-allauth==0.41.0
@@ -18,6 +19,7 @@ gunicorn==20.0.4
idna==2.8
imagesize==1.2.0
Jinja2==2.11.3
+logging-tree==1.8.1
MarkupSafe==1.1.1
oauthlib==3.1.0
packaging==20.0
@@ -31,6 +33,7 @@ python-Levenshtein==0.12.0
python-memcached==1.59
python3-openid==3.1.0
pytz==2019.3
+recommonmark==0.7.1
reportlab==3.5.34
requests==2.22.0
requests-oauthlib==1.3.0
@@ -46,5 +49,5 @@ sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.2
sphinxcontrib-serializinghtml==1.1.3
sqlparse==0.3.0
-urllib3==1.26.5
+urllib3==1.25.11
webencodings==0.5.1
From 09ec97fccc7da250c5442d8b2dd57e8f5fb0223e Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Fri, 26 Nov 2021 02:58:40 +0000
Subject: [PATCH 05/19] Fix requirements.txt
---
apiserver/requirements.txt | 1 -
1 file changed, 1 deletion(-)
diff --git a/apiserver/requirements.txt b/apiserver/requirements.txt
index 6c7cdd9..b8b5b69 100644
--- a/apiserver/requirements.txt
+++ b/apiserver/requirements.txt
@@ -1,6 +1,5 @@
alabaster==0.7.12
argon2-cffi==19.2.0
-asgiref==3.2.3
Babel==2.9.1
bleach==3.3.0
certifi==2019.11.28
From 70764ee53e1273ab3b7ce5143983ad27e68d3bda Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Fri, 26 Nov 2021 03:29:30 +0000
Subject: [PATCH 06/19] Allow secrets to be optional
---
apiserver/apiserver/api/views.py | 4 ++--
apiserver/apiserver/secrets.py.example | 2 ++
apiserver/apiserver/settings.py | 2 +-
apiserver/apiserver/urls.py | 13 ++++++++-----
4 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py
index c0ddd0b..65b6300 100644
--- a/apiserver/apiserver/api/views.py
+++ b/apiserver/apiserver/api/views.py
@@ -446,7 +446,7 @@ class PingView(views.APIView):
class DoorViewSet(viewsets.ViewSet, List):
def list(self, request):
auth_token = request.META.get('HTTP_AUTHORIZATION', '')
- if auth_token != 'Bearer ' + secrets.DOOR_API_TOKEN:
+ if secrets.DOOR_API_TOKEN and auth_token != 'Bearer ' + secrets.DOOR_API_TOKEN:
raise exceptions.PermissionDenied()
cards = models.Card.objects.filter(active_status='card_active')
@@ -488,7 +488,7 @@ class DoorViewSet(viewsets.ViewSet, List):
class LockoutViewSet(viewsets.ViewSet, List):
def list(self, request):
auth_token = request.META.get('HTTP_AUTHORIZATION', '')
- if auth_token != 'Bearer ' + secrets.DOOR_API_TOKEN:
+ if secrets.DOOR_API_TOKEN and auth_token != 'Bearer ' + secrets.DOOR_API_TOKEN:
raise exceptions.PermissionDenied()
cards = models.Card.objects.filter(active_status='card_active')
diff --git a/apiserver/apiserver/secrets.py.example b/apiserver/apiserver/secrets.py.example
index 8196956..7a18316 100644
--- a/apiserver/apiserver/secrets.py.example
+++ b/apiserver/apiserver/secrets.py.example
@@ -1,4 +1,6 @@
# Spaceport secrets file, don't commit to version control!
+#
+# Note: all values are optional, features are excluded if left blank
# /admin/ route obfuscation
# Set this to random characters
diff --git a/apiserver/apiserver/settings.py b/apiserver/apiserver/settings.py
index aa8def4..f6c7090 100644
--- a/apiserver/apiserver/settings.py
+++ b/apiserver/apiserver/settings.py
@@ -25,7 +25,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = secrets.DJANGO_SECRET_KEY
+SECRET_KEY = secrets.DJANGO_SECRET_KEY or 'OaOBN2E+brpoRyDMlTD9eTE5PgBtkkl+L7Bzt6pQ5Qr3GS82SH'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG_ENV = os.environ.get('DEBUG', False)
diff --git a/apiserver/apiserver/urls.py b/apiserver/apiserver/urls.py
index d6165b7..9e5ca02 100644
--- a/apiserver/apiserver/urls.py
+++ b/apiserver/apiserver/urls.py
@@ -7,9 +7,6 @@ from rest_auth.views import LoginView, LogoutView
from .api import views
from . import secrets, settings
-IPN_ROUTE = r'^ipn/{}/'.format(secrets.IPN_RANDOM)
-ADMIN_ROUTE = '{}/admin/'.format(secrets.ADMIN_RANDOM)
-
router = routers.DefaultRouter()
router.register(r'door', views.DoorViewSet, basename='door')
router.register(r'lockout', views.LockoutViewSet, basename='lockout')
@@ -31,7 +28,6 @@ router.register(r'charts/spaceactivity', views.SpaceActivityViewSet, basename='s
urlpatterns = [
path('', include(router.urls)),
- path(ADMIN_ROUTE, admin.site.urls),
url(r'^rest-auth/login/$', LoginView.as_view(), name='rest_login'),
url(r'^spaceport-auth/login/$', views.SpaceportAuthView.as_view(), name='spaceport_auth'),
url(r'^rest-auth/logout/$', LogoutView.as_view(), name='rest_logout'),
@@ -44,9 +40,16 @@ urlpatterns = [
url(r'^ping/', views.PingView.as_view(), name='ping'),
url(r'^paste/', views.PasteView.as_view(), name='paste'),
url(r'^backup/', views.BackupView.as_view(), name='backup'),
- url(IPN_ROUTE, views.IpnView.as_view(), name='ipn'),
]
+if secrets.IPN_RANDOM:
+ IPN_ROUTE = r'^ipn/{}/'.format(secrets.IPN_RANDOM)
+ urlpatterns.append(url(IPN_ROUTE, views.IpnView.as_view(), name='ipn'))
+
+if secrets.ADMIN_RANDOM:
+ ADMIN_ROUTE = '{}/admin/'.format(secrets.ADMIN_RANDOM)
+ urlpatterns.append(path(ADMIN_ROUTE, admin.site.urls))
+
if settings.DEBUG:
urlpatterns += [
path('api-auth/', include('rest_framework.urls')),
From 822cb9ec5c97ba9b43ae13811cb0809ded53c356 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Sun, 28 Nov 2021 05:09:14 +0000
Subject: [PATCH 07/19] Improve username generation
---
webclient/src/LoginSignup.js | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/webclient/src/LoginSignup.js b/webclient/src/LoginSignup.js
index 84d7b00..7710b1e 100644
--- a/webclient/src/LoginSignup.js
+++ b/webclient/src/LoginSignup.js
@@ -81,14 +81,13 @@ export function SignupForm(props) {
const genUsername = () => {
if (input.first_name && input.last_name) {
- let first_name = input.first_name.trim();
- let last_name = input.last_name.trim();
+ let first_name = input.first_name.trim().toLowerCase();
+ let last_name = input.last_name.trim().toLowerCase();
first_name = first_name.replace(/[^a-z- ]+/g, '');
last_name = last_name.replace(/[^a-z- ]+/g, '');
first_name = first_name.replace(/[ -]/g, '.');
last_name = last_name.replace(/[ -]/g, '.');
- const username = first_name + '.' + last_name;
- return username.toLowerCase();
+ return first_name + '.' + last_name;
} else {
return '';
}
From 07559714ff55f4eab6c5a2cb3ad8eb267125a933 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Sun, 28 Nov 2021 21:43:24 +0000
Subject: [PATCH 08/19] Rate limit allow_last_scanned
---
apiserver/apiserver/api/serializers.py | 17 ++++++++++++++++-
webclient/src/Account.js | 7 ++++++-
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py
index 5a0f28c..8c3553f 100644
--- a/apiserver/apiserver/api/serializers.py
+++ b/apiserver/apiserver/api/serializers.py
@@ -3,6 +3,7 @@ logger = logging.getLogger(__name__)
from django.contrib.auth.models import User, Group
from django.shortcuts import get_object_or_404
+from django.utils.timezone import now
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.validators import UniqueValidator
@@ -10,7 +11,7 @@ from rest_auth.registration.serializers import RegisterSerializer
from rest_auth.serializers import PasswordChangeSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, LoginSerializer
from rest_auth.serializers import UserDetailsSerializer
import re
-import time
+import datetime, time
from . import models, fields, utils, utils_ldap, utils_auth, utils_stats
from .. import settings, secrets
@@ -236,6 +237,20 @@ class MemberSerializer(serializers.ModelSerializer):
logger.info(msg)
raise ValidationError(dict(discourse_username='Invalid Discourse username.'))
+ if validated_data.get('allow_last_scanned', None) == True:
+ changed = validated_data['allow_last_scanned'] != instance.allow_last_scanned
+ ONE_WEEK = now() - datetime.timedelta(days=7)
+ if changed and models.HistoryChange.objects.filter(
+ field='allow_last_scanned',
+ index__history_user__member__id=instance.id,
+ index__owner_id=instance.id,
+ index__history_date__gte=ONE_WEEK,
+ ).count() >= 6:
+ msg = 'Member allow_last_scanned rate limit exceeded by: ' + instance.first_name + ' ' + instance.last_name
+ utils.alert_tanner(msg)
+ logger.info(msg)
+ raise ValidationError(dict(allow_last_scanned='You\'re doing that too often.'))
+
return super().update(instance, validated_data)
# admin viewing member details
diff --git a/webclient/src/Account.js b/webclient/src/Account.js
index c32f9f5..a5b226f 100644
--- a/webclient/src/Account.js
+++ b/webclient/src/Account.js
@@ -266,11 +266,16 @@ export function AccountForm(props) {
-
From 065afd966d92bb4c871119d7c129a6c598b2233f Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Mon, 29 Nov 2021 01:19:01 +0000
Subject: [PATCH 09/19] Improve request logging
---
apiserver/apiserver/api/throttles.py | 25 +++++++++++++++++++++++++
apiserver/apiserver/api/views.py | 12 ------------
apiserver/apiserver/settings.py | 2 +-
3 files changed, 26 insertions(+), 13 deletions(-)
create mode 100644 apiserver/apiserver/api/throttles.py
diff --git a/apiserver/apiserver/api/throttles.py b/apiserver/apiserver/api/throttles.py
new file mode 100644
index 0000000..c7b7213
--- /dev/null
+++ b/apiserver/apiserver/api/throttles.py
@@ -0,0 +1,25 @@
+import logging
+logger = logging.getLogger(__name__)
+
+from rest_framework import throttling
+
+class LoggingThrottle(throttling.BaseThrottle):
+ def allow_request(self, request, view):
+ if request.user.id:
+ user = '{} ({})'.format(request.user, request.user.member.id)
+ else:
+ user = None
+
+ method = request._request.method
+ path = request._request.path
+
+ if request.data:
+ data = request.data.dict()
+ for key in ['password', 'password1', 'password2', 'old_password', 'new_password1', 'new_password2']:
+ if key in data:
+ data[key] = '[CENSORED]'
+ else:
+ data = None
+
+ logging.info('Request User: %s | %s %s | Data: %s', user, method, path, data)
+ return True
diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py
index 65b6300..78e37e4 100644
--- a/apiserver/apiserver/api/views.py
+++ b/apiserver/apiserver/api/views.py
@@ -437,9 +437,6 @@ class PingView(views.APIView):
permission_classes = [AllowMetadata | IsAuthenticated]
def post(self, request):
- d = request.data.dict()
- if d:
- logger.info(str(d))
return Response(200)
@@ -739,15 +736,6 @@ class VettingViewSet(Base, List):
class RegistrationView(RegisterView):
serializer_class = serializers.MyRegisterSerializer
- def post(self, request):
- data = request.data.copy()
- data.pop('password1', None)
- data.pop('password2', None)
- logger.info(dict(data))
-
- return super().post(request)
-
-
class PasswordChangeView(PasswordChangeView):
permission_classes = [AllowMetadata | IsAuthenticated]
serializer_class = serializers.MyPasswordChangeSerializer
diff --git a/apiserver/apiserver/settings.py b/apiserver/apiserver/settings.py
index f6c7090..5a2eb61 100644
--- a/apiserver/apiserver/settings.py
+++ b/apiserver/apiserver/settings.py
@@ -55,7 +55,6 @@ SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_REFERRER_POLICY = 'same-origin'
-
# Application definition
INSTALLED_APPS = [
@@ -209,6 +208,7 @@ REST_FRAMEWORK = {
'PAGE_SIZE': 300,
'DEFAULT_RENDERER_CLASSES': DEFAULT_RENDERER_CLASSES,
'DEFAULT_AUTHENTICATION_CLASSES': DEFAULT_AUTHENTICATION_CLASSES,
+ 'DEFAULT_THROTTLE_CLASSES': ['apiserver.api.throttles.LoggingThrottle'],
}
#DEFAULT_LOGGING = None
From e8557cbf37fec6968ed06036167fec08fb7729a4 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Mon, 29 Nov 2021 01:21:52 +0000
Subject: [PATCH 10/19] Ignore noisy requests
---
apiserver/apiserver/api/throttles.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/apiserver/apiserver/api/throttles.py b/apiserver/apiserver/api/throttles.py
index c7b7213..95f4cdb 100644
--- a/apiserver/apiserver/api/throttles.py
+++ b/apiserver/apiserver/api/throttles.py
@@ -13,6 +13,9 @@ class LoggingThrottle(throttling.BaseThrottle):
method = request._request.method
path = request._request.path
+ if path.startswith('/stats/') or path.startswith('/lockout/'):
+ return True
+
if request.data:
data = request.data.dict()
for key in ['password', 'password1', 'password2', 'old_password', 'new_password1', 'new_password2']:
From af3f4ac614db5337fdc5215f2bc648d7a464c842 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Mon, 29 Nov 2021 02:18:58 +0000
Subject: [PATCH 11/19] Log history changes to console
---
apiserver/apiserver/api/serializers.py | 1 +
apiserver/apiserver/api/signals.py | 14 +++++++++++++-
apiserver/apiserver/api/throttles.py | 2 +-
apiserver/apiserver/api/utils_stats.py | 2 +-
apiserver/apiserver/api/views.py | 4 ++--
5 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py
index 8c3553f..d26f2b6 100644
--- a/apiserver/apiserver/api/serializers.py
+++ b/apiserver/apiserver/api/serializers.py
@@ -421,6 +421,7 @@ class CardSerializer(serializers.ModelSerializer):
read_only_fields = [
'id',
'last_seen',
+ 'last_seen_at',
'user',
]
diff --git a/apiserver/apiserver/api/signals.py b/apiserver/apiserver/api/signals.py
index 9acc61a..ae54cac 100644
--- a/apiserver/apiserver/api/signals.py
+++ b/apiserver/apiserver/api/signals.py
@@ -69,7 +69,7 @@ def post_create_historical_record_callback(
is_admin=is_admin_director(history_user),
)
- for change in changes:
+ for num, change in enumerate(changes):
change_old = str(change.old)
change_new = str(change.new)
@@ -84,6 +84,18 @@ def post_create_historical_record_callback(
old=change_old,
new=change_new,
)
+
+ logger.info('History - {} changed {}\'s {} {}/{}: {} "{}" --> "{}"'.format(
+ history_user or 'System',
+ owner[0],
+ object_name,
+ num+1,
+ len(changes),
+ change.field,
+ change_old,
+ change_new,
+ ))
+
except BaseException as e:
logger.error('History Signal - {} - {}'.format(e.__class__.__name__, e))
logger.info(str(sender))
diff --git a/apiserver/apiserver/api/throttles.py b/apiserver/apiserver/api/throttles.py
index 95f4cdb..cf84e0a 100644
--- a/apiserver/apiserver/api/throttles.py
+++ b/apiserver/apiserver/api/throttles.py
@@ -24,5 +24,5 @@ class LoggingThrottle(throttling.BaseThrottle):
else:
data = None
- logging.info('Request User: %s | %s %s | Data: %s', user, method, path, data)
+ logging.info('Request - User: %s | %s %s | Data: %s', user, method, path, data)
return True
diff --git a/apiserver/apiserver/api/utils_stats.py b/apiserver/apiserver/api/utils_stats.py
index 54fc990..48fee23 100644
--- a/apiserver/apiserver/api/utils_stats.py
+++ b/apiserver/apiserver/api/utils_stats.py
@@ -158,7 +158,7 @@ def get_progress(request_id):
return cache.get('request-progress-' + request_id, [])
def set_progress(request_id, data):
- logger.info('Request %s progress: %s', request_id, data)
+ logger.info('Progress - ID: %s | Status: %s', request_id, data)
progress = get_progress(request_id)
progress.append(data)
cache.set('request-progress-' + request_id, progress)
diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py
index 78e37e4..d72895b 100644
--- a/apiserver/apiserver/api/views.py
+++ b/apiserver/apiserver/api/views.py
@@ -475,7 +475,7 @@ class DoorViewSet(viewsets.ViewSet, List):
except models.Member.DoesNotExist:
raise Http404
t = utils.now_alberta_tz().strftime('%Y-%m-%d %H:%M:%S, %a %I:%M %p')
- logger.info('Time: {} - Name: {} {} ({})'.format(t, member.first_name, member.last_name, member.id))
+ logger.info('Scan - Time: {} | Name: {} {} ({})'.format(t, member.first_name, member.last_name, member.id))
utils_stats.calc_card_scans()
@@ -651,7 +651,7 @@ class BackupView(views.APIView):
backup_user = secrets.BACKUP_TOKENS.get(auth_token, None)
if backup_user:
- logger.info('Backup user: ' + backup_user['name'])
+ logger.info('Backup - User: ' + backup_user['name'])
backup_path = cache.get(backup_user['cache_key'], None)
if not backup_path:
From e501427f38110fc3ac7f6976a6c96d6e50ec9d63 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Mon, 29 Nov 2021 02:42:17 +0000
Subject: [PATCH 12/19] Turn down gunicorn logging, reorder request logging
---
apiserver/apiserver/api/throttles.py | 2 +-
apiserver/apiserver/settings.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apiserver/apiserver/api/throttles.py b/apiserver/apiserver/api/throttles.py
index cf84e0a..b62763e 100644
--- a/apiserver/apiserver/api/throttles.py
+++ b/apiserver/apiserver/api/throttles.py
@@ -24,5 +24,5 @@ class LoggingThrottle(throttling.BaseThrottle):
else:
data = None
- logging.info('Request - User: %s | %s %s | Data: %s', user, method, path, data)
+ logging.info('%s %s | User: %s | Data: %s', method, path, user, data)
return True
diff --git a/apiserver/apiserver/settings.py b/apiserver/apiserver/settings.py
index 5a2eb61..0a6949b 100644
--- a/apiserver/apiserver/settings.py
+++ b/apiserver/apiserver/settings.py
@@ -239,7 +239,7 @@ LOGGING = {
'loggers': {
'gunicorn': {
'handlers': ['console'],
- 'level': 'DEBUG',
+ 'level': 'DEBUG' if DEBUG else 'INFO',
'propagate': False,
},
'': {
From 7b5dac7c6e11b40e4b5ef7bccfdaeb434836242e Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Fri, 3 Dec 2021 05:18:41 +0000
Subject: [PATCH 13/19] Add input box for sending messages to the sign
---
apiserver/apiserver/api/utils_stats.py | 1 +
apiserver/apiserver/api/views.py | 17 +++++++++
apiserver/apiserver/secrets.py.example | 1 +
webclient/src/Home.js | 53 ++++++++++++++++++++++++--
4 files changed, 69 insertions(+), 3 deletions(-)
diff --git a/apiserver/apiserver/api/utils_stats.py b/apiserver/apiserver/api/utils_stats.py
index 48fee23..199869f 100644
--- a/apiserver/apiserver/api/utils_stats.py
+++ b/apiserver/apiserver/api/utils_stats.py
@@ -26,6 +26,7 @@ DEFAULTS = {
'card_scans': 0,
'track': {},
'alarm': {},
+ 'sign': '',
}
if secrets.MUMBLE:
diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py
index d72895b..47cd0ad 100644
--- a/apiserver/apiserver/api/views.py
+++ b/apiserver/apiserver/api/views.py
@@ -569,6 +569,23 @@ class StatsViewSet(viewsets.ViewSet, List):
except KeyError:
raise exceptions.ValidationError(dict(data='This field is required.'))
+ @action(detail=False, methods=['post'])
+ def sign(self, request):
+ try:
+ sign = request.data['sign']
+ cache.set('sign', sign)
+
+ try:
+ post_data = dict(access_token=secrets.SIGN_TOKEN, args=sign)
+ r = requests.post('https://api.particle.io/v1/devices/200042000647343232363230/text/', data=post_data, timeout=5)
+ r.raise_for_status()
+ except:
+ raise exceptions.ValidationError(dict(sign='Something went wrong :('))
+
+ return Response(200)
+ except KeyError:
+ raise exceptions.ValidationError(dict(sign='This field is required.'))
+
@action(detail=False, methods=['post'])
def alarm(self, request):
try:
diff --git a/apiserver/apiserver/secrets.py.example b/apiserver/apiserver/secrets.py.example
index 7a18316..5462d5d 100644
--- a/apiserver/apiserver/secrets.py.example
+++ b/apiserver/apiserver/secrets.py.example
@@ -68,6 +68,7 @@ DOOR_CODE = ''
WIFI_PASS = ''
MINECRAFT = ''
MUMBLE = ''
+SIGN_TOKEN = ''
# Portal Email Credentials
# For sending password resets, etc.
diff --git a/webclient/src/Home.js b/webclient/src/Home.js
index 95f4a31..1aaf52f 100644
--- a/webclient/src/Home.js
+++ b/webclient/src/Home.js
@@ -138,6 +138,53 @@ function MemberInfo(props) {
);
};
+function SignForm(props) {
+ const { token } = props;
+ const [error, setError] = useState({});
+ const [sign, setSign] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+
+ const handleValues = (e, v) => setSign(v.value);
+ const handleChange = (e) => handleValues(e, e.currentTarget);
+
+ const handleSubmit = (e) => {
+ if (loading) return;
+ setLoading(true);
+ const data = {sign: sign};
+ requester('/stats/sign/', 'POST', token, data)
+ .then(res => {
+ setLoading(false);
+ setSuccess(true);
+ setError({});
+ setSign('');
+ })
+ .catch(err => {
+ setLoading(false);
+ console.log(err);
+ setError(err.data);
+ });
+ };
+
+ return (
+
+
+ {!!token &&
+ Submit
+ }
+ {success && Success!
}
+
+ );
+};
+
export function Home(props) {
const { user, token } = props;
const [stats, setStats] = useState(JSON.parse(localStorage.getItem('stats', 'false')));
@@ -213,9 +260,6 @@ export function Home(props) {
-
- Welcome to the Protospace member portal! Here you can view member info, join classes, and manage your membership.
-
Main Website
Protospace Wiki — [register]
@@ -306,7 +350,10 @@ export function Home(props) {
{user && Alarm status: {alarmStat()}{doorOpenStat()}
}
+
+
+
From 4079896dc877b4388a161d2119a5a753ce43c1d9 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Fri, 3 Dec 2021 05:20:02 +0000
Subject: [PATCH 14/19] Don't send to sign if it's not configured
---
apiserver/apiserver/api/views.py | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py
index 47cd0ad..73bb320 100644
--- a/apiserver/apiserver/api/views.py
+++ b/apiserver/apiserver/api/views.py
@@ -575,12 +575,13 @@ class StatsViewSet(viewsets.ViewSet, List):
sign = request.data['sign']
cache.set('sign', sign)
- try:
- post_data = dict(access_token=secrets.SIGN_TOKEN, args=sign)
- r = requests.post('https://api.particle.io/v1/devices/200042000647343232363230/text/', data=post_data, timeout=5)
- r.raise_for_status()
- except:
- raise exceptions.ValidationError(dict(sign='Something went wrong :('))
+ if secrets.SIGN_TOKEN:
+ try:
+ post_data = dict(access_token=secrets.SIGN_TOKEN, args=sign)
+ r = requests.post('https://api.particle.io/v1/devices/200042000647343232363230/text/', data=post_data, timeout=5)
+ r.raise_for_status()
+ except:
+ raise exceptions.ValidationError(dict(sign='Something went wrong :('))
return Response(200)
except KeyError:
From 94b030523faaafa13ce576e22061a6859a629555 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Fri, 3 Dec 2021 06:37:17 +0000
Subject: [PATCH 15/19] Keep sign submit button
---
webclient/src/Home.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/webclient/src/Home.js b/webclient/src/Home.js
index 1aaf52f..063b514 100644
--- a/webclient/src/Home.js
+++ b/webclient/src/Home.js
@@ -177,9 +177,9 @@ function SignForm(props) {
error={error.sign}
/>
- {!!token &&
+
Submit
- }
+
{success && Success!
}
);
From db3e1290e40c309161c4043619ee5a8a52aa13c5 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Fri, 3 Dec 2021 06:38:14 +0000
Subject: [PATCH 16/19] Log sign submissions
---
apiserver/apiserver/api/throttles.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/apiserver/apiserver/api/throttles.py b/apiserver/apiserver/api/throttles.py
index b62763e..ade5c33 100644
--- a/apiserver/apiserver/api/throttles.py
+++ b/apiserver/apiserver/api/throttles.py
@@ -13,7 +13,14 @@ class LoggingThrottle(throttling.BaseThrottle):
method = request._request.method
path = request._request.path
- if path.startswith('/stats/') or path.startswith('/lockout/'):
+ if method == 'OPTIONS':
+ return True
+
+ if path.startswith('/lockout/'):
+ return True
+ elif path == '/stats/sign/':
+ pass
+ elif path.startswith('/stats/'):
return True
if request.data:
From 831280d73a249174d256ce0a85c91a0e78572476 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Tue, 7 Dec 2021 04:04:11 +0000
Subject: [PATCH 17/19] Add Utilities page and tool label generator
---
webclient/src/App.js | 6 +--
webclient/src/Paste.js | 95 +++++++++++++++++++++++++++++++++++++++++-
2 files changed, 97 insertions(+), 4 deletions(-)
diff --git a/webclient/src/App.js b/webclient/src/App.js
index b869c8a..35d865c 100644
--- a/webclient/src/App.js
+++ b/webclient/src/App.js
@@ -167,9 +167,9 @@ function App() {
to='/classes'
/>
-
+
diff --git a/webclient/src/Paste.js b/webclient/src/Paste.js
index f8ba118..7d7024b 100644
--- a/webclient/src/Paste.js
+++ b/webclient/src/Paste.js
@@ -41,7 +41,7 @@ function PasteForm(props) {
@@ -53,6 +53,91 @@ function PasteForm(props) {
);
};
+function LabelForm(props) {
+ const [error, setError] = useState(false);
+ const [input, setInput] = useState({ id: '107', size: '2' });
+ const [label, setLabel] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+
+ const handleValues = (e, v) => setInput({ ...input, [v.name]: v.value });
+ const handleChange = (e) => handleValues(e, e.currentTarget);
+
+ const handleSubmit = (e) => {
+ if (loading) return;
+ setLoading(true);
+ fetch('https://decalator-proxy.dns.t0.vc/?' + new URLSearchParams(input))
+ .then(res => {
+ if (res.ok) {
+ return res.blob();
+ } else {
+ return res.text().then(text => {throw new Error(text)});
+ }
+ })
+ .then(res => {
+ setLoading(false);
+ setSuccess(true);
+ setError(false);
+ const imageObjectURL = URL.createObjectURL(res);
+ setLabel(imageObjectURL);
+ })
+ .catch(err => {
+ setLabel(false);
+ setLoading(false);
+ console.log(err);
+ setError(err);
+ });
+ };
+
+ const makeProps = (name) => ({
+ name: name,
+ onChange: handleChange,
+ value: input[name] || '',
+ });
+
+ const sizeOptions = [
+ { key: '0', text: '1.0', value: '1' },
+ { key: '1', text: '1.5', value: '1.5' },
+ { key: '2', text: '2.0', value: '2' },
+ { key: '3', text: '2.5', value: '2.5' },
+ { key: '4', text: '3.0', value: '3' },
+ { key: '5', text: '3.5', value: '3.5' },
+ { key: '6', text: '4.0', value: '4' },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+ Submit
+
+
+ {label && }
+
+ );
+};
+
let pasteCache = 'Loading...';
export function Paste(props) {
@@ -73,6 +158,14 @@ export function Paste(props) {
return (
+
+
+ Use this to generate QR code labels for tools at Protospace.
+
+ Choose a tool from here: https://wiki.protospace.ca/Category:Tools
+
+
+
From 2b6aebc3998c7fce89982d0b14eaf957ecd261a4 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Wed, 8 Dec 2021 22:02:03 +0000
Subject: [PATCH 18/19] Update decalator URL
---
webclient/src/Paste.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/webclient/src/Paste.js b/webclient/src/Paste.js
index 7d7024b..dd8dba9 100644
--- a/webclient/src/Paste.js
+++ b/webclient/src/Paste.js
@@ -66,7 +66,7 @@ function LabelForm(props) {
const handleSubmit = (e) => {
if (loading) return;
setLoading(true);
- fetch('https://decalator-proxy.dns.t0.vc/?' + new URLSearchParams(input))
+ fetch('https://labels.protospace.ca/?' + new URLSearchParams(input))
.then(res => {
if (res.ok) {
return res.blob();
From fd83d171e99b47cfdd1433ac39c76ec34a793291 Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Wed, 8 Dec 2021 22:02:30 +0000
Subject: [PATCH 19/19] Add dedicated Sign page
---
webclient/src/App.js | 5 ++++
webclient/src/Home.js | 48 +-------------------------------
webclient/src/Sign.js | 64 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 70 insertions(+), 47 deletions(-)
create mode 100644 webclient/src/Sign.js
diff --git a/webclient/src/App.js b/webclient/src/App.js
index 35d865c..c850192 100644
--- a/webclient/src/App.js
+++ b/webclient/src/App.js
@@ -16,6 +16,7 @@ import { Training } from './Training.js';
import { AdminTransactions } from './AdminTransactions.js';
import { Admin } from './Admin.js';
import { Paste } from './Paste.js';
+import { Sign } from './Sign.js';
import { Courses, CourseDetail } from './Courses.js';
import { Classes, ClassDetail } from './Classes.js';
import { Members, MemberDetail } from './Members.js';
@@ -219,6 +220,10 @@ function App() {
+
+
+
+
diff --git a/webclient/src/Home.js b/webclient/src/Home.js
index 063b514..427595b 100644
--- a/webclient/src/Home.js
+++ b/webclient/src/Home.js
@@ -7,6 +7,7 @@ import { Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Me
import { statusColor, BasicTable, siteUrl, staticUrl, requester, isAdmin } from './utils.js';
import { LoginForm, SignupForm } from './LoginSignup.js';
import { AccountForm } from './Account.js';
+import { SignForm } from './Sign.js';
import { PayPalSubscribeDeal } from './PayPal.js';
function MemberInfo(props) {
@@ -138,53 +139,6 @@ function MemberInfo(props) {
);
};
-function SignForm(props) {
- const { token } = props;
- const [error, setError] = useState({});
- const [sign, setSign] = useState('');
- const [loading, setLoading] = useState(false);
- const [success, setSuccess] = useState(false);
-
- const handleValues = (e, v) => setSign(v.value);
- const handleChange = (e) => handleValues(e, e.currentTarget);
-
- const handleSubmit = (e) => {
- if (loading) return;
- setLoading(true);
- const data = {sign: sign};
- requester('/stats/sign/', 'POST', token, data)
- .then(res => {
- setLoading(false);
- setSuccess(true);
- setError({});
- setSign('');
- })
- .catch(err => {
- setLoading(false);
- console.log(err);
- setError(err.data);
- });
- };
-
- return (
-
-
-
- Submit
-
- {success && Success!
}
-
- );
-};
-
export function Home(props) {
const { user, token } = props;
const [stats, setStats] = useState(JSON.parse(localStorage.getItem('stats', 'false')));
diff --git a/webclient/src/Sign.js b/webclient/src/Sign.js
new file mode 100644
index 0000000..f4f51f5
--- /dev/null
+++ b/webclient/src/Sign.js
@@ -0,0 +1,64 @@
+import React, { useState, useEffect, useReducer, useContext } from 'react';
+import { BrowserRouter as Router, Switch, Route, Link, useParams, useHistory } from 'react-router-dom';
+import { Button, Container, Checkbox, Dimmer, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react';
+import { apiUrl, statusColor, BasicTable, staticUrl, requester } from './utils.js';
+import { NotFound } from './Misc.js';
+
+export function SignForm(props) {
+ const { token } = props;
+ const [error, setError] = useState({});
+ const [sign, setSign] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+
+ const handleValues = (e, v) => setSign(v.value);
+ const handleChange = (e) => handleValues(e, e.currentTarget);
+
+ const handleSubmit = (e) => {
+ if (loading) return;
+ setLoading(true);
+ const data = {sign: sign};
+ requester('/stats/sign/', 'POST', token, data)
+ .then(res => {
+ setLoading(false);
+ setSuccess(true);
+ setError({});
+ setSign('');
+ })
+ .catch(err => {
+ setLoading(false);
+ console.log(err);
+ setError(err.data);
+ });
+ };
+
+ return (
+
+
+
+ Submit
+
+ {success && Success!
}
+
+ );
+};
+
+export function Sign(props) {
+ const { token } = props;
+
+ return (
+
+
+
+
+
+ );
+};