diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 000000000..a74d99bf5 --- /dev/null +++ b/.flowconfig @@ -0,0 +1,13 @@ +[ignore] + +[include] + +[libs] + +[lints] + +[options] +module.system.node.allow_root_relative=true +module.system.node.root_relative_dirname=./huxley/www/js + +[strict] diff --git a/.gitignore b/.gitignore index 5784b012a..20c890729 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ test_position_paper.doc env/ .vscode/ + +huxley/service.json \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index f891e01cc..b864ae74a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,11 @@ python: - "3.6" install: - pip install -r requirements.txt - - nvm install 6.4 - - nvm use 6.4 + - nvm install 12.19.0 + - nvm use 12.19.0 - npm install before_script: - python manage.py migrate script: - python manage.py test - - npm test + - npm run flow diff --git a/docs/tutorials/setup/tutorial.md b/docs/tutorials/setup/tutorial.md index 1943932dc..caba5c086 100644 --- a/docs/tutorials/setup/tutorial.md +++ b/docs/tutorials/setup/tutorial.md @@ -181,7 +181,7 @@ var TodoApp = React.createClass({ } }); -module.exports = TodoApp; +export default TodoApp; ``` Then in www.html change the line `

Hello, world!

` to `
` diff --git a/huxley/api/permissions.py b/huxley/api/permissions.py index 851f9ed01..ec9657e13 100644 --- a/huxley/api/permissions.py +++ b/huxley/api/permissions.py @@ -120,7 +120,8 @@ def has_permission(self, request, view): return ( user_is_advisor(request, view, assignment.registration.school_id) or user_is_chair(request, view, assignment.committee_id) or - user_is_delegate(request, view, assignment_id, 'assignment')) + user_is_delegate(request, view, assignment_id, 'assignment') or + user_is_in_committee(request, view, assignment.committee_id)) class AssignmentListPermission(permissions.BasePermission): @@ -132,10 +133,31 @@ def has_permission(self, request, view): school_id = request.query_params.get('school_id', -1) committee_id = request.query_params.get('committee_id', -1) return (user_is_chair(request, view, committee_id) or - user_is_advisor(request, view, school_id)) + user_is_advisor(request, view, school_id) or + user_is_in_committee(request, view, committee_id)) return False +class CommitteeDetailPermission(permissions.BasePermission): + '''Accept requests to retrieve a committee from any user. + Only allow superusers and chairs to update committees.''' + + def has_permission(self, request, view): + if request.user.is_superuser: + return True + + committee_id = view.kwargs.get('pk', None) + committee = Committee.objects.get(id=committee_id) + user = request.user + method = request.method + + if method != 'GET': + return user_is_chair(request, view, + committee_id) + + return True + + class DelegateDetailPermission(permissions.BasePermission): '''Accept requests to retrieve, update, and destroy a delegate from the @@ -185,7 +207,8 @@ def has_permission(self, request, view): school_id = request.query_params.get('school_id', -1) committee_id = request.query_params.get('committee_id', -1) return (user_is_chair(request, view, committee_id) or - user_is_advisor(request, view, school_id)) + user_is_advisor(request, view, school_id) or + user_is_delegate(request, view, committee_id, 'committee')) if method == 'POST': return user_is_advisor(request, view, request.data['school']) @@ -331,6 +354,14 @@ def has_permission(self, request, view): return False +class NotePermission(permissions.BasePermission): + '''Accept requests to view Notes when user is not an advisor''' + def has_permission(self, request, view): + user = request.user + if user.is_authenticated and not user.is_advisor(): + return True + return False + def user_is_advisor(request, view, school_id): user = request.user @@ -351,7 +382,15 @@ def user_is_delegate(request, view, target_id, field=None): if not user.is_authenticated or not user.is_delegate(): return False - if field: + if field and field != 'committee': return getattr(user.delegate, field + '_id', -1) == int(target_id) + elif field == 'committee': + return getattr(getattr(user.delegate, 'assignment', -1), 'committee_id', -1) == int(target_id) return user.delegate_id == int(target_id) + +def user_is_in_committee(request, view, committee_id): + user = request.user + if not user.is_authenticated or not user.is_delegate() or not user.delegate: + return False + return user.delegate.assignment.committee_id == int(committee_id) diff --git a/huxley/api/serializers/__init__.py b/huxley/api/serializers/__init__.py index 2db53d4f6..3027f9493 100644 --- a/huxley/api/serializers/__init__.py +++ b/huxley/api/serializers/__init__.py @@ -12,3 +12,4 @@ from .position_paper import PositionPaperSerializer from .rubric import RubricSerializer from .secretariat_member import SecretariatMemberSerializer +from .note import NoteSerializer \ No newline at end of file diff --git a/huxley/api/serializers/committee.py b/huxley/api/serializers/committee.py index 1f496f091..51a136d62 100644 --- a/huxley/api/serializers/committee.py +++ b/huxley/api/serializers/committee.py @@ -17,4 +17,6 @@ class Meta: 'full_name', 'delegation_size', 'rubric', - 'special', ) + 'special', + 'zoom_link', + 'notes_activated') diff --git a/huxley/api/serializers/note.py b/huxley/api/serializers/note.py new file mode 100644 index 000000000..f88202156 --- /dev/null +++ b/huxley/api/serializers/note.py @@ -0,0 +1,16 @@ +# Copyright (c) 2011-2020 Berkeley Model United Nations. All rights reserved. +# Use of this source code is governed by a BSD License (see LICENSE). + +from rest_framework import serializers + +from huxley.core.models import Note + +class TimestampField(serializers.ReadOnlyField): + def to_representation(self, value): + return value.timestamp.timestamp() + +class NoteSerializer(serializers.ModelSerializer): + timestamp = TimestampField(source='*') + class Meta: + model = Note + fields = ('id', 'is_chair', 'sender', 'recipient', 'msg', 'timestamp') diff --git a/huxley/api/tests/test_committee.py b/huxley/api/tests/test_committee.py index fdce1aa9d..57398a134 100644 --- a/huxley/api/tests/test_committee.py +++ b/huxley/api/tests/test_committee.py @@ -1,6 +1,6 @@ # Copyright (c) 2011-2015 Berkeley Model United Nations. All rights reserved. # Use of this source code is governed by a BSD License (see LICENSE). - +from huxley.accounts.models import User from huxley.api import tests from huxley.api.tests import auto from huxley.utils.test import models @@ -22,20 +22,52 @@ class CommitteeDetailPutTestCase(tests.UpdateAPITestCase): params = {'name': 'DISC', 'special': True} def setUp(self): - self.committee = models.new_committee() + self.chair = models.new_user(username='chair', + password='chair', + user_type=User.TYPE_CHAIR) + self.committee = models.new_committee(user=self.chair) def test_anonymous_user(self): '''Unauthenticated users shouldn't be able to update committees.''' response = self.get_response(self.committee.id, params=self.params) - self.assertMethodNotAllowed(response, 'PUT') + self.assertNotAuthenticated(response) - def test_authenticated_user(self): - '''Authenticated users shouldn't be able to update committees.''' - models.new_user(username='user', password='user') - self.client.login(username='user', password='user') + def test_delegate(self): + '''Delegates shouldn't be able to update committees.''' + models.new_user(username='delegate', + password='user', + user_type=User.TYPE_DELEGATE) + self.client.login(username='delegate', password='user') + + response = self.get_response(self.committee.id, params=self.params) + self.assertPermissionDenied(response) + + def test_advisor(self): + '''Advisors shouldn't be able to update committees.''' + models.new_user(username='advisor', + password='user', + user_type=User.TYPE_ADVISOR) + self.client.login(username='advisor', password='user') + + response = self.get_response(self.committee.id, params=self.params) + self.assertPermissionDenied(response) + + def test_chair(self): + '''Chairs should be able to update committees.''' + self.client.login(username='chair', password='chair') response = self.get_response(self.committee.id, params=self.params) - self.assertMethodNotAllowed(response, 'PUT') + response.data.pop('rubric') + self.assertEqual( + response.data, { + 'id': self.committee.id, + 'name': 'DISC', + 'full_name': self.committee.full_name, + 'delegation_size': self.committee.delegation_size, + 'special': True, + 'notes_activated': self.committee.notes_activated, + 'zoom_link': self.committee.zoom_link + }) def test_superuser(self): '''Superusers shouldn't be able to update committees.''' @@ -43,7 +75,17 @@ def test_superuser(self): self.client.login(username='user', password='user') response = self.get_response(self.committee.id, params=self.params) - self.assertMethodNotAllowed(response, 'PUT') + response.data.pop('rubric') + self.assertEqual( + response.data, { + 'id': self.committee.id, + 'name': 'DISC', + 'full_name': self.committee.full_name, + 'delegation_size': self.committee.delegation_size, + 'special': True, + 'notes_activated': self.committee.notes_activated, + 'zoom_link': self.committee.zoom_link + }) class CommitteeDetailPatchTestCase(tests.PartialUpdateAPITestCase): @@ -51,20 +93,52 @@ class CommitteeDetailPatchTestCase(tests.PartialUpdateAPITestCase): params = {'name': 'DISC', 'special': True} def setUp(self): - self.committee = models.new_committee() + self.chair = models.new_user(username='chair', + password='chair', + user_type=User.TYPE_CHAIR) + self.committee = models.new_committee(user=self.chair) def test_anonymous_user(self): '''Unauthenticated users shouldn't be able to update committees.''' response = self.get_response(self.committee.id, params=self.params) - self.assertMethodNotAllowed(response, 'PATCH') + self.assertNotAuthenticated(response) - def test_authenticated_user(self): - '''Authenticated users shouldn't be able to update committees.''' - models.new_user(username='user', password='user') - self.client.login(username='user', password='user') + def test_delegate(self): + '''Delegates shouldn't be able to update committees.''' + models.new_user(username='delegate', + password='user', + user_type=User.TYPE_DELEGATE) + self.client.login(username='delegate', password='user') + + response = self.get_response(self.committee.id, params=self.params) + self.assertPermissionDenied(response) + + def test_advisor(self): + '''Advisors shouldn't be able to update committees.''' + models.new_user(username='advisor', + password='user', + user_type=User.TYPE_ADVISOR) + self.client.login(username='advisor', password='user') + + response = self.get_response(self.committee.id, params=self.params) + self.assertPermissionDenied(response) + + def test_chair(self): + '''Chairs should be able to update committees.''' + self.client.login(username='chair', password='chair') response = self.get_response(self.committee.id, params=self.params) - self.assertMethodNotAllowed(response, 'PATCH') + response.data.pop('rubric') + self.assertEqual( + response.data, { + 'id': self.committee.id, + 'name': 'DISC', + 'full_name': self.committee.full_name, + 'delegation_size': self.committee.delegation_size, + 'special': True, + 'notes_activated': self.committee.notes_activated, + 'zoom_link': self.committee.zoom_link + }) def test_superuser(self): '''Superusers shouldn't be able to update committees.''' @@ -72,7 +146,17 @@ def test_superuser(self): self.client.login(username='user', password='user') response = self.get_response(self.committee.id, params=self.params) - self.assertMethodNotAllowed(response, 'PATCH') + response.data.pop('rubric') + self.assertEqual( + response.data, { + 'id': self.committee.id, + 'name': 'DISC', + 'full_name': self.committee.full_name, + 'delegation_size': self.committee.delegation_size, + 'special': True, + 'notes_activated': self.committee.notes_activated, + 'zoom_link': self.committee.zoom_link + }) class CommitteeDetailDeleteTestCase(auto.DestroyAPIAutoTestCase): @@ -84,12 +168,12 @@ def get_test_object(cls): def test_anonymous_user(self): '''Anonymous users cannot delete committees.''' - self.do_test(expected_error=auto.EXP_DELETE_NOT_ALLOWED) + self.do_test(expected_error=auto.EXP_NOT_AUTHENTICATED) def test_authenticated_user(self): '''Authenticated users cannot delete committees.''' self.as_default_user().do_test( - expected_error=auto.EXP_DELETE_NOT_ALLOWED) + expected_error=auto.EXP_PERMISSION_DENIED) def test_superuser(self): '''Superusers cannot delete committees.''' @@ -107,24 +191,32 @@ def test_anonymous_user(self): response = self.get_response() for r in response.data: r.pop('rubric') - self.assertEqual(response.data, [ - {'delegation_size': c1.delegation_size, - 'special': c1.special, - 'id': c1.id, - 'full_name': c1.full_name, - 'name': c1.name}, {'delegation_size': c2.delegation_size, - 'special': c2.special, - 'id': c2.id, - 'full_name': c2.full_name, - 'name': c2.name} - ]) + self.assertEqual(response.data, [{ + 'delegation_size': c1.delegation_size, + 'special': c1.special, + 'id': c1.id, + 'full_name': c1.full_name, + 'name': c1.name, + 'notes_activated': c1.notes_activated, + 'zoom_link': c1.zoom_link + }, { + 'delegation_size': c2.delegation_size, + 'special': c2.special, + 'id': c2.id, + 'full_name': c2.full_name, + 'name': c2.name, + 'notes_activated': c2.notes_activated, + 'zoom_link': c2.zoom_link + }]) class CommitteeListPostTestCase(tests.CreateAPITestCase): url_name = 'api:committee_list' - params = {'name': 'DISC', - 'full_name': 'Disarmament and International Security', - 'delegation_size': 100} + params = { + 'name': 'DISC', + 'full_name': 'Disarmament and International Security', + 'delegation_size': 100 + } def test_anonymous_user(self): '''Unauthenticated users shouldn't be able to create committees.''' diff --git a/huxley/api/tests/test_delegate.py b/huxley/api/tests/test_delegate.py index a7de51885..5937ae2a4 100644 --- a/huxley/api/tests/test_delegate.py +++ b/huxley/api/tests/test_delegate.py @@ -447,7 +447,8 @@ def setUp(self): registration=self.registration, committee=self.committee) self.assignment2 = models.new_assignment( registration=self.registration) - self.delegate1 = models.new_delegate(assignment=self.assignment1, ) + self.delegate1 = models.new_delegate(assignment=self.assignment1, + user=self.delegate_user) self.delegate2 = models.new_delegate( assignment=self.assignment2, name='Trevor Dowds', @@ -492,7 +493,7 @@ def test_delegate(self): response = self.get_response( params={'committee_id': self.committee.id}) - self.assertPermissionDenied(response) + self.assert_delegates_equal(response, [self.delegate1]) def test_other_user(self): '''It rejects a request from another user.''' diff --git a/huxley/api/tests/test_registration.py b/huxley/api/tests/test_registration.py index d1217dae1..3f12607d5 100644 --- a/huxley/api/tests/test_registration.py +++ b/huxley/api/tests/test_registration.py @@ -22,6 +22,7 @@ class RegistrationListPostTest(tests.CreateAPITestCase): def test_valid(self): school = models.new_school() conference = Conference.get_current() + _, _ = models.new_country(), models.new_country() params = self.get_params() params['school'] = school.id params['conference'] = conference.session @@ -85,6 +86,7 @@ def test_fees(self): '''Fees should be read-only fields.''' school = models.new_school() conference = Conference.get_current() + _, _, _ = models.new_country(), models.new_country(), models.new_country() params = self.get_params( school=school.id, conference=conference.session, diff --git a/huxley/api/tests/test_user.py b/huxley/api/tests/test_user.py index 9d24c58a4..e22d11434 100644 --- a/huxley/api/tests/test_user.py +++ b/huxley/api/tests/test_user.py @@ -96,11 +96,12 @@ def test_self(self): def test_chair(self): '''It should have the correct fields for chairs.''' + committee = models.new_committee() user = models.new_user( username='testuser', password='test', user_type=User.TYPE_CHAIR, - committee_id=4) + committee_id=committee.id) self.client.login(username='testuser', password='test') response = self.get_response(user.id) diff --git a/huxley/api/urls.py b/huxley/api/urls.py index 9f647ea1b..5562a7238 100644 --- a/huxley/api/urls.py +++ b/huxley/api/urls.py @@ -89,6 +89,12 @@ url(r'^secretariat_member_committee/?$', views.secretariat_member.SecretariatMemberCommitteeList.as_view(), name='secretariat_member_committee_list'), + url(r'^notes/?$', + views.note.NoteList.as_view(), + name='note_list'), + url(r'^note/?$', + views.note.NoteDetail.as_view(), + name='note_detail') ] urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/huxley/api/views/__init__.py b/huxley/api/views/__init__.py index b3397bd75..d5ea180fe 100644 --- a/huxley/api/views/__init__.py +++ b/huxley/api/views/__init__.py @@ -1,4 +1,4 @@ # Copyright (c) 2011-2015 Berkeley Model United Nations. All rights reserved. # Use of this source code is governed by a BSD License (see LICENSE). -from huxley.api.views import assignment, committee, committee_feedback, country, delegate, school, user, position_paper, registration, register, rubric, secretariat_member +from huxley.api.views import assignment, committee, committee_feedback, country, delegate, school, user, position_paper, registration, register, rubric, secretariat_member, note diff --git a/huxley/api/views/committee.py b/huxley/api/views/committee.py index e863c2bbe..d281c0ba7 100644 --- a/huxley/api/views/committee.py +++ b/huxley/api/views/committee.py @@ -4,6 +4,7 @@ from rest_framework import generics from rest_framework.authentication import SessionAuthentication +from huxley.api import permissions from huxley.api.serializers import CommitteeSerializer from huxley.core.models import Committee @@ -14,7 +15,14 @@ class CommitteeList(generics.ListAPIView): serializer_class = CommitteeSerializer -class CommitteeDetail(generics.RetrieveAPIView): +class CommitteeDetail(generics.RetrieveUpdateAPIView): authentication_classes = (SessionAuthentication,) queryset = Committee.objects.all() + permission_classes = (permissions.CommitteeDetailPermission, ) serializer_class = CommitteeSerializer + + def patch(self, request, *args, **kwargs): + return super().patch(request, args, kwargs) + + def put(self, request, *args, **kwargs): + return self.partial_update(request, *args, **kwargs) diff --git a/huxley/api/views/note.py b/huxley/api/views/note.py new file mode 100644 index 000000000..cf0cc7608 --- /dev/null +++ b/huxley/api/views/note.py @@ -0,0 +1,66 @@ +# Copyright (c) 2011-2021 Berkeley Model United Nations. All rights reserved. +# Use of this source code is governed by a BSD License (see LICENSE). +import datetime + +from rest_framework import generics, status +from rest_framework.authentication import SessionAuthentication +from rest_framework.exceptions import PermissionDenied +from rest_framework.response import Response + +from huxley.api.mixins import ListUpdateModelMixin +from huxley.api import permissions +from huxley.api.serializers import NoteSerializer +from huxley.core.models import Assignment, Conference, Note + + +class NoteList(generics.ListCreateAPIView): + authentication_classes = (SessionAuthentication, ) + permission_classes = (permissions.NotePermission, ) + serializer_class = NoteSerializer + + def get_queryset(self): + queryset = Note.objects.all() + query_params = self.request.GET + sender_id = query_params.get('sender_id', None) + timestamp = query_params.get('timestamp', None) + committee_id = query_params.get('committee_id', None) + + if not timestamp: + return Note.objects.none() + + # Divide by 1000 because fromtimestamp takes in value in seconds + timestamp_date = datetime.datetime.fromtimestamp( + int(timestamp) / 1000.0) + + if committee_id and timestamp: + queryset = queryset.filter( + sender__committee_id=committee_id).filter( + timestamp__gte=timestamp_date) | queryset.filter( + recipient__committee_id=committee_id).filter( + timestamp__gte=timestamp_date) + + if sender_id and timestamp: + queryset = queryset.filter(sender_id=sender_id).filter( + timestamp__gte=timestamp_date) | queryset.filter( + recipient_id=sender_id).filter( + timestamp__gte=timestamp_date) + + return queryset + + +class NoteDetail(generics.CreateAPIView, generics.RetrieveAPIView): + authentication_classes = (SessionAuthentication, ) + queryset = Note.objects.all() + permission_classes = (permissions.NotePermission, ) + serializer_class = NoteSerializer + + def post(self, request, *args, **kwargs): + conference = Conference.get_current() + if not conference.notes_enabled: + return Response({'reason': 'Notes for this conference are currently off'}, status=status.HTTP_403_FORBIDDEN) + if request.data['sender'] and request.data['recipient']: + sender = Assignment.objects.get(id=request.data['sender']) + committee = sender.committee + if not committee.notes_activated: + return Response({'reason': 'The chair has disabled notes for this committee'}, status=status.HTTP_403_FORBIDDEN) + return super().post(request, args, kwargs) \ No newline at end of file diff --git a/huxley/core/admin/__init__.py b/huxley/core/admin/__init__.py index c8fbea61e..1fa53a005 100644 --- a/huxley/core/admin/__init__.py +++ b/huxley/core/admin/__init__.py @@ -27,3 +27,4 @@ admin.site.register(Rubric) admin.site.register(PositionPaper, PositionPaperAdmin) admin.site.register(SecretariatMember, SecretariatMemberAdmin) +admin.site.register(Note) \ No newline at end of file diff --git a/huxley/core/admin/assignment.py b/huxley/core/admin/assignment.py index f00f1b203..ba9556861 100644 --- a/huxley/core/admin/assignment.py +++ b/huxley/core/admin/assignment.py @@ -1,53 +1,57 @@ -# Copyright (c) 2011-2015 Berkeley Model United Nations. All rights reserved. +# Copyright (c) 2011-2021 Berkeley Model United Nations. All rights reserved. # Use of this source code is governed by a BSD License (see LICENSE). import csv +from django.conf import settings from django.conf.urls import url from django.urls import reverse from django.contrib import admin, messages from django.http import HttpResponse, HttpResponseRedirect from django.utils import html +from googleapiclient.discovery import build +from google.oauth2 import service_account + from huxley.core.models import Assignment, Committee, Country, School, PositionPaper class AssignmentAdmin(admin.ModelAdmin): - search_fields = ( - 'country__name', - 'registration__school__name', - 'committee__name', - 'committee__full_name' - ) + search_fields = ('country__name', 'registration__school__name', + 'committee__name', 'committee__full_name') + + def get_rows(self): + rows = [] + rows.append(['School', 'Committee', 'Country', 'Rejected']) + + for assignment in Assignment.objects.all().order_by( + 'registration__school__name', 'committee__name'): + rows.append([ + str(item) for item in [ + assignment.registration.school, assignment.committee, + assignment.country, assignment.rejected + ] + ]) + + return rows def list(self, request): '''Return a CSV file containing the current country assignments.''' assignments = HttpResponse(content_type='text/csv') - assignments['Content-Disposition'] = 'attachment; filename="assignments.csv"' + assignments[ + 'Content-Disposition'] = 'attachment; filename="assignments.csv"' writer = csv.writer(assignments) - writer.writerow([ - 'School', - 'Committee', - 'Country', - 'Rejected' - ]) - - for assignment in Assignment.objects.all().order_by('registration__school__name', - 'committee__name'): - writer.writerow([ - assignment.registration.school, - assignment.committee, - assignment.country, - assignment.rejected - ]) + for row in self.get_rows(): + writer.writerow(row) return assignments def load(self, request): '''Loads new Assignments.''' assignments = request.FILES - reader = csv.reader(assignments['csv'].read().decode('utf-8').splitlines()) + reader = csv.reader( + assignments['csv'].read().decode('utf-8').splitlines()) def get_model(model, name, cache): name = name.strip() @@ -67,52 +71,91 @@ def generate_assignments(reader): if len(row) == 0: continue - if (row[0]=='School' and row[1]=='Committee' and row[2]=='Country'): - continue # skip the first row if it is a header - + if (row[0] == 'School' and row[1] == 'Committee' + and row[2] == 'Country'): + continue # skip the first row if it is a header + while len(row) < 3: - row.append("") # extend the row to have the minimum proper num of columns + row.append( + "" + ) # extend the row to have the minimum proper num of columns if len(row) < 4: - rejected = False # allow for the rejected field to be null + rejected = False # allow for the rejected field to be null else: - rejected = (row[3].lower() == 'true') # use the provided value if admin provides it + rejected = ( + row[3].lower() == 'true' + ) # use the provided value if admin provides it committee = get_model(Committee, row[1], committees) country = get_model(Country, row[2], countries) school = get_model(School, row[0], schools) yield (committee, country, school, rejected) - - failed_rows = Assignment.update_assignments(generate_assignments(reader)) + failed_rows = Assignment.update_assignments( + generate_assignments(reader)) if failed_rows: # Format the message with HTML to put each failed assignment on a new line - messages.error(request, - html.format_html('Assignment upload aborted. These assignments failed:
' + '
'.join(failed_rows))) - - return HttpResponseRedirect(reverse('admin:core_assignment_changelist')) + messages.error( + request, + html.format_html( + 'Assignment upload aborted. These assignments failed:
' + + '
'.join(failed_rows))) + + return HttpResponseRedirect( + reverse('admin:core_assignment_changelist')) + + def sheets(self, request): + if settings.SHEET_ID: + SHEET_RANGE = 'Assignments!A1:D' + # Store credentials + creds = service_account.Credentials.from_service_account_file( + settings.SERVICE_ACCOUNT_FILE, scopes=settings.SCOPES) + + data = self.get_rows() + + body = { + 'values': data, + } + + service = build('sheets', 'v4', credentials=creds) + response = service.spreadsheets().values().clear( + spreadsheetId=settings.SHEET_ID, + range=SHEET_RANGE, + ).execute() + + response = service.spreadsheets().values().update( + spreadsheetId=settings.SHEET_ID, + range=SHEET_RANGE, + valueInputOption='USER_ENTERED', + body=body).execute() + + return HttpResponseRedirect( + reverse('admin:core_assignment_changelist')) def get_urls(self): return super(AssignmentAdmin, self).get_urls() + [ - url( - r'list', + url(r'list', self.admin_site.admin_view(self.list), - name='core_assignment_list' - ), + name='core_assignment_list'), url( r'load', self.admin_site.admin_view(self.load), name='core_assignment_load', ), + url( + r'sheets', + self.admin_site.admin_view(self.sheets), + name='core_assignment_sheets', + ), ] def delete_model(self, request, obj): '''Deletes Rubric objects when individual committees are deleted''' super().delete_model(request, obj) - PositionPaper.objects.filter(assignment = None).delete() + PositionPaper.objects.filter(assignment=None).delete() def delete_queryset(self, request, queryset): '''Deletes Rubric objects when queryset of committees are deleted''' super().delete_queryset(request, queryset) - PositionPaper.objects.filter(assignment = None).delete() - \ No newline at end of file + PositionPaper.objects.filter(assignment=None).delete() diff --git a/huxley/core/admin/committee_feedback.py b/huxley/core/admin/committee_feedback.py index 95f0589f8..06d7b8352 100644 --- a/huxley/core/admin/committee_feedback.py +++ b/huxley/core/admin/committee_feedback.py @@ -1,11 +1,16 @@ -# Copyright (c) 2011-2017 Berkeley Model United Nations. All rights reserved. +# Copyright (c) 2011-2021 Berkeley Model United Nations. All rights reserved. # Use of this source code is governed by a BSD License (see LICENSE). import csv +from django.conf import settings from django.conf.urls import url from django.contrib import admin -from django.http import HttpResponse +from django.urls import reverse +from django.http import HttpResponse, HttpResponseRedirect + +from googleapiclient.discovery import build +from google.oauth2 import service_account from huxley.core.models import CommitteeFeedback @@ -14,95 +19,92 @@ class CommitteeFeedbackAdmin(admin.ModelAdmin): search_fields = ('committee__name', ) - def list(self, request): - '''Return a CSV file containing all committee feedback.''' - feedbacks = HttpResponse(content_type='text/csv') - feedbacks[ - 'Content-Disposition'] = 'attachment; filename="feedback.csv"' - writer = csv.writer(feedbacks) - writer.writerow([ - 'Committee', - 'General Rating', - 'General Comment', - 'Chair 1', - 'Chair 1 Rating', - 'Chair 1 Comment', - 'Chair 2 Name', - 'Chair 2 Rating', - 'Chair 2 Comment', - 'Chair 3 Name', - 'Chair 3 Rating', - 'Chair 3 Comment', - 'Chair 4 Name', - 'Chair 4 Rating', - 'Chair 4 Comment', - 'Chair 5 Name', - 'Chair 5 Rating', - 'Chair 5 Comment', - 'Chair 6 Name', - 'Chair 6 Rating', - 'Chair 6 Comment', - 'Chair 7 Name', - 'Chair 7 Rating', - 'Chair 7 Comment', - 'Chair 8 Name', - 'Chair 8 Rating', - 'Chair 8 Comment', - 'Chair 9 Name', - 'Chair 9 Rating', - 'Chair 9 Comment', - 'Chair 10 Name', - 'Chair 10 Rating', - 'Chair 10 Comment', - 'Perception of Berkeley', + def get_rows(self): + rows = [] + rows.append([ + 'Committee', 'General Rating', 'General Comment', 'Chair 1', + 'Chair 1 Rating', 'Chair 1 Comment', 'Chair 2 Name', + 'Chair 2 Rating', 'Chair 2 Comment', 'Chair 3 Name', + 'Chair 3 Rating', 'Chair 3 Comment', 'Chair 4 Name', + 'Chair 4 Rating', 'Chair 4 Comment', 'Chair 5 Name', + 'Chair 5 Rating', 'Chair 5 Comment', 'Chair 6 Name', + 'Chair 6 Rating', 'Chair 6 Comment', 'Chair 7 Name', + 'Chair 7 Rating', 'Chair 7 Comment', 'Chair 8 Name', + 'Chair 8 Rating', 'Chair 8 Comment', 'Chair 9 Name', + 'Chair 9 Rating', 'Chair 9 Comment', 'Chair 10 Name', + 'Chair 10 Rating', 'Chair 10 Comment', 'Perception of Berkeley', 'Money Spent' ]) for feedback in CommitteeFeedback.objects.all().order_by( 'committee__name'): - writer.writerow([ - feedback.committee.name, - feedback.rating, - feedback.comment, - feedback.chair_1_name, - feedback.chair_1_rating, - feedback.chair_1_comment, - feedback.chair_2_name, - feedback.chair_2_rating, - feedback.chair_2_comment, - feedback.chair_3_name, - feedback.chair_3_rating, - feedback.chair_3_comment, - feedback.chair_4_name, - feedback.chair_4_rating, - feedback.chair_4_comment, - feedback.chair_5_name, - feedback.chair_5_rating, - feedback.chair_5_comment, - feedback.chair_6_name, - feedback.chair_6_rating, - feedback.chair_6_comment, - feedback.chair_7_name, - feedback.chair_7_rating, - feedback.chair_7_comment, - feedback.chair_8_name, - feedback.chair_8_rating, - feedback.chair_8_comment, - feedback.chair_9_name, - feedback.chair_9_rating, - feedback.chair_9_comment, - feedback.chair_10_name, - feedback.chair_10_rating, - feedback.chair_10_comment, - feedback.berkeley_perception, - feedback.money_spent + rows.append([ + feedback.committee.name, feedback.rating, feedback.comment, + feedback.chair_1_name, feedback.chair_1_rating, + feedback.chair_1_comment, feedback.chair_2_name, + feedback.chair_2_rating, feedback.chair_2_comment, + feedback.chair_3_name, feedback.chair_3_rating, + feedback.chair_3_comment, feedback.chair_4_name, + feedback.chair_4_rating, feedback.chair_4_comment, + feedback.chair_5_name, feedback.chair_5_rating, + feedback.chair_5_comment, feedback.chair_6_name, + feedback.chair_6_rating, feedback.chair_6_comment, + feedback.chair_7_name, feedback.chair_7_rating, + feedback.chair_7_comment, feedback.chair_8_name, + feedback.chair_8_rating, feedback.chair_8_comment, + feedback.chair_9_name, feedback.chair_9_rating, + feedback.chair_9_comment, feedback.chair_10_name, + feedback.chair_10_rating, feedback.chair_10_comment, + feedback.berkeley_perception, feedback.money_spent ]) + return rows + + def list(self, request): + '''Return a CSV file containing all committee feedback.''' + feedbacks = HttpResponse(content_type='text/csv') + feedbacks[ + 'Content-Disposition'] = 'attachment; filename="feedback.csv"' + writer = csv.writer(feedbacks) + for row in self.get_rows(): + writer.writerow(row) return feedbacks + def sheets(self, request): + if settings.SHEET_ID: + SHEET_RANGE = 'Feedback!A1:AI' + # Store credentials + creds = service_account.Credentials.from_service_account_file( + settings.SERVICE_ACCOUNT_FILE, scopes=settings.SCOPES) + + data = self.get_rows() + + body = { + 'values': data, + } + + service = build('sheets', 'v4', credentials=creds) + response = service.spreadsheets().values().clear( + spreadsheetId=settings.SHEET_ID, + range=SHEET_RANGE, + ).execute() + response = service.spreadsheets().values().update( + spreadsheetId=settings.SHEET_ID, + range=SHEET_RANGE, + valueInputOption='USER_ENTERED', + body=body).execute() + + return HttpResponseRedirect( + reverse('admin:core_committeefeedback_changelist')) + def get_urls(self): return super(CommitteeFeedbackAdmin, self).get_urls() + [ url(r'list', self.admin_site.admin_view(self.list), - name='core_committeefeedback_list') + name='core_committeefeedback_list'), + url( + r'sheets', + self.admin_site.admin_view(self.sheets), + name='core_committeefeedback_sheets', + ), ] diff --git a/huxley/core/admin/delegate.py b/huxley/core/admin/delegate.py index d77326915..0fc3a2c8c 100644 --- a/huxley/core/admin/delegate.py +++ b/huxley/core/admin/delegate.py @@ -1,13 +1,17 @@ -# Copyright (c) 2011-2015 Berkeley Model United Nations. All rights reserved. +# Copyright (c) 2011-2021 Berkeley Model United Nations. All rights reserved. # Use of this source code is governed by a BSD License (see LICENSE). import csv +from django.conf import settings from django.conf.urls import url from django.contrib import admin, messages from django.urls import reverse from django.http import HttpResponse, HttpResponseRedirect +from googleapiclient.discovery import build +from google.oauth2 import service_account + from huxley.core.models import Assignment, Delegate, School @@ -15,25 +19,36 @@ class DelegateAdmin(admin.ModelAdmin): search_fields = ('name', ) - def roster(self, request): - '''Return a CSV file representing the entire roster of registered - delegates, including their committee, country, and school.''' - roster = HttpResponse(content_type='text/csv') - roster['Content-Disposition'] = 'attachment; filename="roster.csv"' - writer = csv.writer(roster) - writer.writerow([ - 'Name', 'School', 'Committee', 'Country', 'Email', 'Waiver?', + def get_rows(self): + rows = [] + + rows.append([ + 'ID', 'Name', 'School', 'Committee', 'Country', 'Email', 'Waiver?', 'Session One', 'Session Two', 'Session Three', 'Session Four' ]) ordering = 'assignment__registration__school__name' for delegate in Delegate.objects.all().order_by(ordering): - writer.writerow([ - delegate, delegate.school, delegate.committee, - delegate.country, delegate.email, delegate.waiver_submitted, + rows.append([ + str(delegate.id), + str(delegate), + str(delegate.school), + str(delegate.committee), + str(delegate.country), + str(delegate.email), delegate.waiver_submitted, delegate.session_one, delegate.session_two, delegate.session_three, delegate.session_four ]) + return rows + + def roster(self, request): + '''Return a CSV file representing the entire roster of registered + delegates, including their committee, country, and school.''' + roster = HttpResponse(content_type='text/csv') + roster['Content-Disposition'] = 'attachment; filename="roster.csv"' + writer = csv.writer(roster) + for row in self.get_rows(): + writer.writerow(row) return roster @@ -44,11 +59,11 @@ def load(self, request): ''' existing_delegates = Delegate.objects.all() delegates = request.FILES - reader = csv.reader(delegates['csv'].read().decode('utf-8').splitlines()) + reader = csv.reader( + delegates['csv'].read().decode('utf-8').splitlines()) assignments = {} for assignment in Assignment.objects.all(): - assignments[assignment.committee.name, - assignment.country.name, + assignments[assignment.committee.name, assignment.country.name, assignment.registration.school.name, ] = assignment for row in reader: @@ -56,21 +71,26 @@ def load(self, request): if row[1] == 'Committee': continue school = School.objects.get(name=str(row[3])) - assignment = assignments[str( - row[1]), str(row[2]), row[3], ] + assignment = assignments[str(row[1]), str(row[2]), row[3], ] email = str(row[4]) - delg = list(Delegate.objects.filter(name=str(row[0]), email=email)) + delg = list( + Delegate.objects.filter(name=str(row[0]), email=email)) if len(delg) == 1: - Delegate.objects.filter(name=str(row[0]), email=email).update(assignment=assignment) + Delegate.objects.filter(name=str( + row[0]), email=email).update(assignment=assignment) else: - Delegate.objects.create(name=row[0], school=school, email=email, assignment=assignment) + Delegate.objects.create(name=row[0], + school=school, + email=email, + assignment=assignment) return HttpResponseRedirect(reverse('admin:core_delegate_changelist')) def confirm_waivers(self, request): '''Confirms delegate waivers''' waiver_responses = request.FILES - reader = csv.reader(waiver_responses['csv'].read().decode('utf-8').splitlines()) + reader = csv.reader( + waiver_responses['csv'].read().decode('utf-8').splitlines()) rows_to_write = [] @@ -124,24 +144,72 @@ def confirm_waivers(self, request): writer.writerow(['Number of \'Email is duplicated\'', duplicate]) writer.writerow([]) - writer.writerow([ - 'Email', 'Name', 'School', 'Committee', 'Country', 'Error' - ]) + writer.writerow( + ['Email', 'Name', 'School', 'Committee', 'Country', 'Error']) for row in rows_to_write: writer.writerow(row) return waiver_input_response + def sheets(self, request): + if settings.SHEET_ID: + SHEET_RANGE = 'Delegates!A1:K' + # Store credentials + creds = service_account.Credentials.from_service_account_file( + settings.SERVICE_ACCOUNT_FILE, scopes=settings.SCOPES) + + service = build('sheets', 'v4', credentials=creds) + + response = service.spreadsheets().values().get( + spreadsheetId=settings.SHEET_ID, + range=SHEET_RANGE, + majorDimension='ROWS').execute() + + if 'values' in response and len(response['values']) > 1: + for row in response['values'][1:]: + delegate = Delegate.objects.get(id=row[0]) + if row[6] == "TRUE" or row[6] == "x": + delegate.waiver_submitted = True + delegate.save() + + data = self.get_rows() + + body = { + 'values': data, + } + response = service.spreadsheets().values().clear( + spreadsheetId=settings.SHEET_ID, + range=SHEET_RANGE, + ).execute() + service.spreadsheets().values().update( + spreadsheetId=settings.SHEET_ID, + range=SHEET_RANGE, + valueInputOption='USER_ENTERED', + body=body).execute() + + return HttpResponseRedirect(reverse('admin:core_delegate_changelist')) + def get_urls(self): return super(DelegateAdmin, self).get_urls() + [ - url(r'roster', + url( + r'roster', self.admin_site.admin_view(self.roster), - name='core_delegate_roster', ), - url(r'load', + name='core_delegate_roster', + ), + url( + r'load', self.admin_site.admin_view(self.load), - name='core_delegate_load', ), - url(r'confirm_waivers', + name='core_delegate_load', + ), + url( + r'confirm_waivers', self.admin_site.admin_view(self.confirm_waivers), - name='core_delegate_confirm_waivers', ), + name='core_delegate_confirm_waivers', + ), + url( + r'sheets', + self.admin_site.admin_view(self.sheets), + name='core_delegate_sheets', + ), ] diff --git a/huxley/core/admin/registration.py b/huxley/core/admin/registration.py index 1dd55314e..e0510df22 100644 --- a/huxley/core/admin/registration.py +++ b/huxley/core/admin/registration.py @@ -1,70 +1,116 @@ -# Copyright (c) 2011-2017 Berkeley Model United Nations. All rights reserved. +# Copyright (c) 2011-2021 Berkeley Model United Nations. All rights reserved. # Use of this source code is governed by a BSD License (see LICENSE). import csv +from django.conf import settings from django.conf.urls import url from django.contrib import admin -from django.http import HttpResponse +from django.urls import reverse +from django.http import HttpResponse, HttpResponseRedirect + +from googleapiclient.discovery import build +from google.oauth2 import service_account from huxley.core.models import Registration class RegistrationAdmin(admin.ModelAdmin): - def info(self, request): - '''Returns a CSV file of all the registration information.''' - registrations = HttpResponse(content_type='text/csv') - registrations[ - 'Content-Disposition'] = 'attachment; filename="registration_info.csv"' - - writer = csv.writer(registrations) - writer.writerow([ + def get_rows(self): + rows = [] + rows.append([ "Registration Time", "School Name", "Total Number of Delegates", "Beginners", "Intermediates", "Advanced", "Spanish Speakers", - "Chinese Speakers", "Assignments Finalized", "Waivers Complete", - "Delegate Fees Paid", "Delegate Fees Owed", "Paid Registration Fee?", - "Country 1", "Country 2", "Country 3", "Country 4", "Country 5", - "Country 6", "Country 7", "Country 8", "Country 9", "Country 10", - "Committee Preferences", "Registration Comments" + "Chinese Speakers", "Assignments Finalized", "Waivers Complete", + "Delegate Fees Paid", "Delegate Fees Owed", + "Paid Registration Fee?", "Country 1", "Country 2", "Country 3", + "Country 4", "Country 5", "Country 6", "Country 7", "Country 8", + "Country 9", "Country 10", "Committee Preferences", + "Registration Comments" ]) for registration in Registration.objects.all().order_by( 'school__name'): country_preferences = [ - cp + str(cp) for cp in registration.country_preferences.all().order_by( 'countrypreference') ] country_preferences += [''] * (10 - len(country_preferences)) - committee_preferences = [', '.join( - cp.name for cp in registration.committee_preferences.all())] - - writer.writerow( - [str(field) - for field in [ - registration.registered_at, - registration.school.name, - registration.num_beginner_delegates + - registration.num_intermediate_delegates + - registration.num_advanced_delegates, - registration.num_beginner_delegates, - registration.num_intermediate_delegates, - registration.num_advanced_delegates, - registration.num_spanish_speaking_delegates, - registration.num_chinese_speaking_delegates, - registration.assignments_finalized, - registration.waivers_completed, - registration.delegate_fees_paid, - registration.delegate_fees_owed, - registration.registration_fee_paid - ]] + country_preferences + committee_preferences + [str( - registration.registration_comments)]) + committee_preferences = [ + ', '.join(cp.name + for cp in registration.committee_preferences.all()) + ] + + rows.append([ + str(field) for field in [ + registration.registered_at, registration.school.name, + registration.num_beginner_delegates + + registration.num_intermediate_delegates + + registration.num_advanced_delegates, + registration.num_beginner_delegates, + registration.num_intermediate_delegates, + registration.num_advanced_delegates, + registration.num_spanish_speaking_delegates, + registration.num_chinese_speaking_delegates, registration. + assignments_finalized, registration.waivers_completed, + registration.delegate_fees_paid, registration. + delegate_fees_owed, registration.registration_fee_paid + ] + ] + country_preferences + committee_preferences + + [str(registration.registration_comments)]) + return rows + + def info(self, request): + '''Returns a CSV file of all the registration information.''' + registrations = HttpResponse(content_type='text/csv') + registrations[ + 'Content-Disposition'] = 'attachment; filename="registration_info.csv"' + + writer = csv.writer(registrations) + + for row in self.get_rows(): + writer.writerow(row) return registrations + def sheets(self, request): + if settings.SHEET_ID: + SHEET_RANGE = 'Registration!A1:Y' + # Store credentials + creds = service_account.Credentials.from_service_account_file( + settings.SERVICE_ACCOUNT_FILE, scopes=settings.SCOPES) + + data = self.get_rows() + + body = { + 'values': data, + } + + service = build('sheets', 'v4', credentials=creds) + response = service.spreadsheets().values().clear( + spreadsheetId=settings.SHEET_ID, + range=SHEET_RANGE, + ).execute() + response = service.spreadsheets().values().update( + spreadsheetId=settings.SHEET_ID, + range=SHEET_RANGE, + valueInputOption='USER_ENTERED', + body=body).execute() + + return HttpResponseRedirect( + reverse('admin:core_registration_changelist')) + def get_urls(self): return super(RegistrationAdmin, self).get_urls() + [ - url(r'info', + url( + r'info', self.admin_site.admin_view(self.info), - name='core_registration_info', ) + name='core_registration_info', + ), + url( + r'sheets', + self.admin_site.admin_view(self.sheets), + name='core_registration_sheets', + ), ] diff --git a/huxley/core/admin/schools.py b/huxley/core/admin/schools.py index 7dc038d2b..a3e0ce84c 100644 --- a/huxley/core/admin/schools.py +++ b/huxley/core/admin/schools.py @@ -1,11 +1,16 @@ -# Copyright (c) 2011-2015 Berkeley Model United Nations. All rights reserved. +# Copyright (c) 2011-2021 Berkeley Model United Nations. All rights reserved. # Use of this source code is governed by a BSD License (see LICENSE). import csv +from django.conf import settings from django.conf.urls import url from django.contrib import admin -from django.http import HttpResponse +from django.urls import reverse +from django.http import HttpResponse, HttpResponseRedirect + +from googleapiclient.discovery import build +from google.oauth2 import service_account from huxley.core.models import School @@ -14,14 +19,9 @@ class SchoolAdmin(admin.ModelAdmin): search_fields = ('name', ) - def info(self, request): - ''' Returns a CSV file containing the current set of - Schools in our database with all of its fields. ''' - schools = HttpResponse(content_type='text/csv') - schools['Content-Disposition'] = 'attachment; filename="schools.csv"' - writer = csv.writer(schools) - - writer.writerow([ + def get_rows(self): + rows = [] + rows.append([ "ID", "Name", "Address", @@ -45,35 +45,79 @@ def info(self, request): ]) for school in School.objects.all().order_by('name'): - writer.writerow([str(field) - for field in [ - school.id, - school.name, - school.address, - school.city, - school.state, - school.zip_code, - school.country, - school.primary_name, - school.primary_gender, - school.primary_email, - school.primary_phone, - school.primary_type, - school.secondary_name, - school.secondary_gender, - school.secondary_email, - school.secondary_phone, - school.secondary_type, - school.program_type, - school.times_attended, - school.international, - ]]) + rows.append([ + str(field) for field in [ + school.id, + school.name, + school.address, + school.city, + school.state, + school.zip_code, + school.country, + school.primary_name, + school.primary_gender, + school.primary_email, + school.primary_phone, + school.primary_type, + school.secondary_name, + school.secondary_gender, + school.secondary_email, + school.secondary_phone, + school.secondary_type, + school.program_type, + school.times_attended, + school.international, + ] + ]) + return rows + + def info(self, request): + ''' Returns a CSV file containing the current set of + Schools in our database with all of its fields. ''' + schools = HttpResponse(content_type='text/csv') + schools['Content-Disposition'] = 'attachment; filename="schools.csv"' + writer = csv.writer(schools) + for row in self.get_rows(): + writer.writerow(row) return schools + def sheets(self, request): + if settings.SHEET_ID: + SHEET_RANGE = 'Schools!A1:T' + # Store credentials + creds = service_account.Credentials.from_service_account_file( + settings.SERVICE_ACCOUNT_FILE, scopes=settings.SCOPES) + + data = self.get_rows() + + body = { + 'values': data, + } + + service = build('sheets', 'v4', credentials=creds) + response = service.spreadsheets().values().clear( + spreadsheetId=settings.SHEET_ID, + range=SHEET_RANGE, + ).execute() + response = service.spreadsheets().values().update( + spreadsheetId=settings.SHEET_ID, + range=SHEET_RANGE, + valueInputOption='USER_ENTERED', + body=body).execute() + + return HttpResponseRedirect(reverse('admin:core_school_changelist')) + def get_urls(self): return super(SchoolAdmin, self).get_urls() + [ - url(r'info', + url( + r'info', self.admin_site.admin_view(self.info), - name='core_school_info', ) + name='core_school_info', + ), + url( + r'sheets', + self.admin_site.admin_view(self.sheets), + name='core_school_sheets', + ), ] diff --git a/huxley/core/migrations/0047_note.py b/huxley/core/migrations/0047_note.py new file mode 100644 index 000000000..be2594c8c --- /dev/null +++ b/huxley/core/migrations/0047_note.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.6 on 2021-01-09 12:09 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0046_auto_20200919_1744'), + ] + + operations = [ + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_chair', models.SmallIntegerField(choices=[(0, 0), (1, 1), (2, 2)], default=0)), + ('msg', models.CharField(max_length=1000)), + ('recipient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='core.Assignment')), + ('sender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='core.Assignment')), + ], + ), + ] diff --git a/huxley/core/migrations/0048_auto_20210207_1345.py b/huxley/core/migrations/0048_auto_20210207_1345.py new file mode 100644 index 000000000..22b81b3f3 --- /dev/null +++ b/huxley/core/migrations/0048_auto_20210207_1345.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.6 on 2021-02-07 13:45 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0047_note'), + ] + + operations = [ + migrations.AlterModelOptions( + name='note', + options={'ordering': ['timestamp']}, + ), + migrations.AddField( + model_name='note', + name='timestamp', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AlterModelTable( + name='note', + table='note', + ), + ] diff --git a/huxley/core/migrations/0049_committee_notes_activated.py b/huxley/core/migrations/0049_committee_notes_activated.py new file mode 100644 index 000000000..3342001b0 --- /dev/null +++ b/huxley/core/migrations/0049_committee_notes_activated.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2021-02-22 18:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0048_auto_20210207_1345'), + ] + + operations = [ + migrations.AddField( + model_name='committee', + name='notes_activated', + field=models.BooleanField(default=False), + ), + ] diff --git a/huxley/core/migrations/0050_conference_notes_enabled.py b/huxley/core/migrations/0050_conference_notes_enabled.py new file mode 100644 index 000000000..ee35dfa33 --- /dev/null +++ b/huxley/core/migrations/0050_conference_notes_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2021-02-24 18:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0049_committee_notes_activated'), + ] + + operations = [ + migrations.AddField( + model_name='conference', + name='notes_enabled', + field=models.BooleanField(default=False), + ), + ] diff --git a/huxley/core/migrations/0051_auto_20210225_0043.py b/huxley/core/migrations/0051_auto_20210225_0043.py new file mode 100644 index 000000000..84e2a2d0c --- /dev/null +++ b/huxley/core/migrations/0051_auto_20210225_0043.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.6 on 2021-02-25 00:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0050_conference_notes_enabled'), + ] + + operations = [ + migrations.AddField( + model_name='committee', + name='zoom_link', + field=models.URLField(default='https://zoom.us'), + ), + migrations.AddField( + model_name='conference', + name='opi_link', + field=models.URLField(default='https://zoom.us'), + ), + ] diff --git a/huxley/core/migrations/0052_auto_20210225_0044.py b/huxley/core/migrations/0052_auto_20210225_0044.py new file mode 100644 index 000000000..6c4f390e3 --- /dev/null +++ b/huxley/core/migrations/0052_auto_20210225_0044.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2021-02-25 00:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0051_auto_20210225_0043'), + ] + + operations = [ + migrations.AddField( + model_name='committee', + name='zoom_link', + field=models.URLField(default='https://zoom.us'), + ), + ] diff --git a/huxley/core/models.py b/huxley/core/models.py index a0e1e06b6..8fb77a949 100644 --- a/huxley/core/models.py +++ b/huxley/core/models.py @@ -37,18 +37,20 @@ class Conference(models.Model): open_reg = models.BooleanField(default=True) waitlist_reg = models.BooleanField(default=False) position_papers_accepted = models.BooleanField(default=False) + notes_enabled = models.BooleanField(default=False) early_paper_deadline = models.DateField() paper_deadline = models.DateField() waiver_avail_date = models.DateField() waiver_deadline = models.DateField() waiver_link = models.CharField(max_length=300) + opi_link = models.URLField(default="https://zoom.us") external = models.CharField(max_length=128) treasurer = models.CharField(max_length=128) registration_fee = models.DecimalField( max_digits=6, decimal_places=2, default=Decimal('50.00')) delegate_fee = models.DecimalField( max_digits=6, decimal_places=2, default=Decimal('50.00')) - + @classmethod def get_current(cls): return Conference.objects.get(session=settings.SESSION) @@ -120,6 +122,8 @@ class Committee(models.Model): delegation_size = models.PositiveSmallIntegerField(default=2) special = models.BooleanField(default=False) rubric = models.OneToOneField(Rubric, on_delete=models.SET_NULL, blank=True, null=True) + zoom_link = models.URLField(default="https://zoom.us") + notes_activated = models.BooleanField(default=False) @classmethod def create_rubric(cls, **kwargs): @@ -685,3 +689,40 @@ class SecretariatMember(models.Model): def __str__(self): return self.name + +class Note(models.Model): + """Note objects allow delegates to send and receive notes over Huxley.""" + + #TODO add an export of notes so can reference note content later should there be any legal issues. + + #is_chair - 0: Between two Assignments, 1: Sender is Chair, 2: Recipient is Chair + is_chair = models.SmallIntegerField(default= 0, choices = ((0, 0), (1, 1), (2, 2))) + sender = models.ForeignKey(Assignment, on_delete=models.CASCADE, null=True, blank=True, related_name = '+') + recipient = models.ForeignKey(Assignment, on_delete=models.CASCADE, null=True, blank=True, related_name = '+') + msg = models.CharField(max_length = 1000) + timestamp = models.DateTimeField(auto_now_add=True) + + def __str__(self): + committee, sender, recipient = "", "", "" + if self.is_chair == 0: + committee = str(self.sender.committee) + sender = str(self.sender.country) + recipient = str(self.recipient.country) + + elif self.is_chair == 1: + committee = str(self.recipient.committee) + sender = 'Chair' + recipient = str(self.recipient.country) + + else: + committee = str(self.sender.committee) + sender = str(self.sender.country) + recipient = 'Chair' + + + return committee + ": " + sender + ' -> ' + recipient + ' - ' + str(self.id) + + class Meta: + db_table = u'note' + ordering = ['timestamp'] + \ No newline at end of file diff --git a/huxley/core/tests/models/test_models.py b/huxley/core/tests/models/test_models.py index 7ff93fd21..cad67c536 100644 --- a/huxley/core/tests/models/test_models.py +++ b/huxley/core/tests/models/test_models.py @@ -10,7 +10,7 @@ from huxley.core.models import ( Assignment, Committee, CommitteeFeedback, Conference, Country, - CountryPreference, Delegate, PositionPaper, Rubric, SecretariatMember) + CountryPreference, Delegate, Note, PositionPaper, Rubric, SecretariatMember) from huxley.utils.test import models @@ -194,7 +194,10 @@ def test_update_assignment(self): def test_create_position_paper(self): '''Tests that an assigment creates a new position paper upon being saved for the first time, but not on subsequent saves.''' - a = Assignment(committee_id=1, country_id=1, registration_id=1) + committee = models.new_committee() + registration = models.new_registration() + country = models.new_country() + a = Assignment(committee_id=committee.id, country_id=country.id, registration_id=registration.id) self.assertTrue(a.paper == None) a.save() self.assertTrue(a.paper != None) @@ -240,6 +243,15 @@ def test_save(self): school=school, assignment=assignment) +def NoteTest(TestCase): + + fixtures = ['conference'] + + def test_timestamp(self): + '''Ensure that timestamp is automatically created on model save''' + note = models.new_note() + self.assertIsNotNone(note.timestamp) + class RegistrationTest(TestCase): diff --git a/huxley/settings/local.py.default b/huxley/settings/local.py.default index 61fa53368..29c863dce 100644 --- a/huxley/settings/local.py.default +++ b/huxley/settings/local.py.default @@ -33,3 +33,5 @@ EMAIL_HOST_USER = 'email_host_user' # Username for the mail server. EMAIL_HOST_PASSWORD = 'email_host_password' # Password DEFAULT_FROM_EMAIL = 'default_from_email' # Default email for the From: field. SERVER_EMAIL = 'server_email' # Inbox on the server. + +SHEET_ID = None # Spreadsheet ID for Google Sheets integration diff --git a/huxley/settings/main.py b/huxley/settings/main.py index 74ac98f1a..e19b04095 100644 --- a/huxley/settings/main.py +++ b/huxley/settings/main.py @@ -131,3 +131,8 @@ # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_ROOT = '%s/static/' % HUXLEY_ROOT STATIC_URL = '/static/' + +# Google Sheets integration +SCOPES = ['https://www.googleapis.com/auth/spreadsheets'] +SERVICE_ACCOUNT_FILE = 'huxley/service.json' +SHEET_ID = None diff --git a/huxley/templates/admin/core/assignment/change_list.html b/huxley/templates/admin/core/assignment/change_list.html index 38ec8313f..e7b5083e2 100644 --- a/huxley/templates/admin/core/assignment/change_list.html +++ b/huxley/templates/admin/core/assignment/change_list.html @@ -17,6 +17,11 @@ Download Assignment List +
  • + + Update Sheets + +
  • {% endblock %} {% endif %} diff --git a/huxley/templates/admin/core/committeefeedback/change_list.html b/huxley/templates/admin/core/committeefeedback/change_list.html index 7caa7c916..8dfdd4718 100644 --- a/huxley/templates/admin/core/committeefeedback/change_list.html +++ b/huxley/templates/admin/core/committeefeedback/change_list.html @@ -17,6 +17,11 @@ Download Feedback +
  • + + Update Sheets + +
  • {% endblock %} {% endif %} diff --git a/huxley/templates/admin/core/delegate/change_list.html b/huxley/templates/admin/core/delegate/change_list.html index 804ec40db..97e07e7eb 100644 --- a/huxley/templates/admin/core/delegate/change_list.html +++ b/huxley/templates/admin/core/delegate/change_list.html @@ -17,6 +17,11 @@ Download Roster +
  • + + Update Sheets + +
  • {% endblock %} {% endif %} diff --git a/huxley/templates/admin/core/registration/change_list.html b/huxley/templates/admin/core/registration/change_list.html index 144e72366..89c7d26e7 100644 --- a/huxley/templates/admin/core/registration/change_list.html +++ b/huxley/templates/admin/core/registration/change_list.html @@ -17,6 +17,11 @@ Download Registration Info +
  • + + Update Sheets + +
  • {% endblock %} {% endif %} diff --git a/huxley/templates/admin/core/school/change_list.html b/huxley/templates/admin/core/school/change_list.html index c8eae6075..85b222687 100644 --- a/huxley/templates/admin/core/school/change_list.html +++ b/huxley/templates/admin/core/school/change_list.html @@ -17,6 +17,11 @@ Download School Info +
  • + + Update Sheets + +
  • {% endblock %} {% endif %} diff --git a/huxley/utils/test/models.py b/huxley/utils/test/models.py index 620347528..30582f5ce 100644 --- a/huxley/utils/test/models.py +++ b/huxley/utils/test/models.py @@ -9,7 +9,7 @@ from huxley.accounts.models import User from huxley.core.constants import ContactGender, ContactType, ProgramTypes -from huxley.core.models import School, Committee, CommitteeFeedback, Country, Delegate, Assignment, Registration, Conference, PositionPaper, Rubric, SecretariatMember +from huxley.core.models import School, Committee, CommitteeFeedback, Country, Delegate, Assignment, Registration, Conference, PositionPaper, Rubric, SecretariatMember, Note if not settings.TESTING: raise PermissionDenied @@ -40,25 +40,26 @@ def new_superuser(*args, **kwargs): def new_school(**kwargs): - s = School( - name=kwargs.pop('name', 'Test School'), - address=kwargs.pop('address', '1 Schoolhouse Road'), - city=kwargs.pop('city', 'Berkeley'), - state=kwargs.pop('state', 'CA'), - zip_code=kwargs.pop('zip_code', '94024'), - country=kwargs.pop('country', 'United States of America'), - primary_name=kwargs.pop('primary_name', 'first'), - primary_gender=kwargs.pop('primary_gender', ContactGender.MALE), - primary_email=kwargs.pop('primary_email', 'e@mail.com'), - primary_phone=kwargs.pop('primary_phone', '1234567890'), - primary_type=kwargs.pop('primary_type', ContactType.FACULTY), - secondary_name=kwargs.pop('secondary_name', ''), - secondary_gender=kwargs.pop('secondary_gender', ContactGender.MALE), - secondary_email=kwargs.pop('secondary_email', ''), - secondary_phone=kwargs.pop('secondary_phone', ''), - secondary_type=kwargs.pop('secondary_type', ContactType.FACULTY), - program_type=kwargs.pop('program_type', ProgramTypes.CLUB), - times_attended=kwargs.pop('times_attended', 0)) + s = School(name=kwargs.pop('name', 'Test School'), + address=kwargs.pop('address', '1 Schoolhouse Road'), + city=kwargs.pop('city', 'Berkeley'), + state=kwargs.pop('state', 'CA'), + zip_code=kwargs.pop('zip_code', '94024'), + country=kwargs.pop('country', 'United States of America'), + primary_name=kwargs.pop('primary_name', 'first'), + primary_gender=kwargs.pop('primary_gender', ContactGender.MALE), + primary_email=kwargs.pop('primary_email', 'e@mail.com'), + primary_phone=kwargs.pop('primary_phone', '1234567890'), + primary_type=kwargs.pop('primary_type', ContactType.FACULTY), + secondary_name=kwargs.pop('secondary_name', ''), + secondary_gender=kwargs.pop('secondary_gender', + ContactGender.MALE), + secondary_email=kwargs.pop('secondary_email', ''), + secondary_phone=kwargs.pop('secondary_phone', ''), + secondary_type=kwargs.pop('secondary_type', + ContactType.FACULTY), + program_type=kwargs.pop('program_type', ProgramTypes.CLUB), + times_attended=kwargs.pop('times_attended', 0)) user = kwargs.pop('user', None) for attr, value in kwargs.items(): @@ -77,11 +78,10 @@ def new_school(**kwargs): def new_committee(**kwargs): - c = Committee( - name=kwargs.pop('name', 'testCommittee'), - full_name=kwargs.pop('fullName', 'testCommittee'), - delegation_size=kwargs.pop('delegation_size', 10), - special=kwargs.pop('special', False)) + c = Committee(name=kwargs.pop('name', 'testCommittee'), + full_name=kwargs.pop('fullName', 'testCommittee'), + delegation_size=kwargs.pop('delegation_size', 10), + special=kwargs.pop('special', False)) c.save() user = kwargs.pop('user', None) @@ -91,8 +91,9 @@ def new_committee(**kwargs): c.save() if user is None: - new_user( - username=str(uuid.uuid4()), committee=c, user_type=User.TYPE_CHAIR) + new_user(username=str(uuid.uuid4()), + committee=c, + user_type=User.TYPE_CHAIR) else: user.committee = c user.save() @@ -135,7 +136,7 @@ def new_committee_feedback(**kwargs): chair_9_rating=kwargs.pop('chair_9_rating', 0), chair_10_name=kwargs.pop('chair_10_name', ""), chair_10_comment=kwargs.pop('chair_10_comment', ""), - chair_10_rating=kwargs.pop('chair_10_rating', 0), + chair_10_rating=kwargs.pop('chair_10_rating', 0), berkeley_perception=kwargs.pop('berkeley_perception', 0), money_spent=kwargs.pop('money_spent', 0)) feedback.save() @@ -143,9 +144,8 @@ def new_committee_feedback(**kwargs): def new_country(**kwargs): - c = Country( - name=kwargs.pop('name', 'TestCountry'), - special=kwargs.pop('special', False)) + c = Country(name=kwargs.pop('name', 'TestCountry'), + special=kwargs.pop('special', False)) c.save() return c @@ -160,7 +160,8 @@ def new_delegate(**kwargs): school=s, name=kwargs.pop('name', 'Nate Parke'), email=kwargs.pop('email', 'nate@earthlink.gov'), - summary=kwargs.pop('summary', 'He did well!'), ) + summary=kwargs.pop('summary', 'He did well!'), + ) d.save() if user: @@ -180,7 +181,8 @@ def new_assignment(**kwargs): registration=test_registration, country=test_country, paper=test_paper, - rejected=kwargs.pop('rejected', False), ) + rejected=kwargs.pop('rejected', False), + ) a.save() return a @@ -225,7 +227,29 @@ def new_secretariat_member(**kwargs): sm = SecretariatMember( name=test_name, committee=test_committee, - is_head_chair=test_is_head_chair, ) + is_head_chair=test_is_head_chair, + ) sm.save() return sm + + +def new_note(**kwargs): + # None is a valid value for sender & recipient + test_sender = kwargs.pop('sender', -1) + test_sender = test_sender if test_sender != -1 else new_delegate() + + test_recipient = kwargs.pop('recipient', -1) + test_recipient = test_recipient if test_recipient != -1 else new_delegate() + + test_is_chair = kwargs.pop('is_chair', None) or 0 + + test_msg = kwargs.pop('msg', None) or 'hello' + + note = Note(sender=test_sender, + recipient=test_recipient, + is_chair=test_is_chair, + msg=test_msg) + + note.save() + return note diff --git a/huxley/www/js/actions/AssignmentActions.js b/huxley/www/js/actions/AssignmentActions.js index d98f529fd..cc8a85c51 100644 --- a/huxley/www/js/actions/AssignmentActions.js +++ b/huxley/www/js/actions/AssignmentActions.js @@ -3,10 +3,10 @@ * Use of this source code is governed by a BSD License (see LICENSE). */ -'use strict'; +"use strict"; -var ActionConstants = require('constants/ActionConstants'); -var Dispatcher = require('dispatcher/Dispatcher'); +import ActionConstants from "constants/ActionConstants"; +import { Dispatcher } from "dispatcher/Dispatcher"; var AssignmentActions = { assignmentsFetched(assignments) { @@ -26,4 +26,4 @@ var AssignmentActions = { }, }; -module.exports = AssignmentActions; +export { AssignmentActions }; diff --git a/huxley/www/js/actions/CommitteeActions.js b/huxley/www/js/actions/CommitteeActions.js index 9aabf6914..4d191588a 100644 --- a/huxley/www/js/actions/CommitteeActions.js +++ b/huxley/www/js/actions/CommitteeActions.js @@ -3,10 +3,10 @@ * Use of this source code is governed by a BSD License (see LICENSE). */ -'use strict'; +"use strict"; -var ActionConstants = require('constants/ActionConstants'); -var Dispatcher = require('dispatcher/Dispatcher'); +import ActionConstants from "constants/ActionConstants"; +import { Dispatcher } from "dispatcher/Dispatcher"; var CommitteeActions = { committeesFetched(committees) { @@ -15,6 +15,15 @@ var CommitteeActions = { committees: committees, }); }, + + updateCommittee(committeeID, delta, onError) { + Dispatcher.dispatch({ + actionType: ActionConstants.UPDATE_COMMITTEE, + committeeID: committeeID, + delta: delta, + onError: onError, + }); + }, }; -module.exports = CommitteeActions; +export { CommitteeActions }; diff --git a/huxley/www/js/actions/CommitteeFeedbackActions.js b/huxley/www/js/actions/CommitteeFeedbackActions.js index 60ea5f431..381499f72 100644 --- a/huxley/www/js/actions/CommitteeFeedbackActions.js +++ b/huxley/www/js/actions/CommitteeFeedbackActions.js @@ -3,10 +3,10 @@ * Use of this source code is governed by a BSD License (see LICENSE). */ -'use strict'; +"use strict"; -var ActionConstants = require('constants/ActionConstants'); -var Dispatcher = require('dispatcher/Dispatcher'); +import ActionConstants from "constants/ActionConstants"; +import { Dispatcher } from "dispatcher/Dispatcher"; var CommitteeFeedbackActions = { addCommitteeFeedback(feedback) { @@ -24,4 +24,4 @@ var CommitteeFeedbackActions = { }, }; -module.exports = CommitteeFeedbackActions; +export { CommitteeFeedbackActions }; diff --git a/huxley/www/js/actions/CountryActions.js b/huxley/www/js/actions/CountryActions.js index c31df34a8..6409b912f 100644 --- a/huxley/www/js/actions/CountryActions.js +++ b/huxley/www/js/actions/CountryActions.js @@ -3,10 +3,10 @@ * Use of this source code is governed by a BSD License (see LICENSE). */ -'use strict'; +"use strict"; -var ActionConstants = require('constants/ActionConstants'); -var Dispatcher = require('dispatcher/Dispatcher'); +import ActionConstants from "constants/ActionConstants"; +import { Dispatcher } from "dispatcher/Dispatcher"; var CountryActions = { countriesFetched(countries) { @@ -17,4 +17,4 @@ var CountryActions = { }, }; -module.exports = CountryActions; +export { CountryActions }; diff --git a/huxley/www/js/actions/CurrentUserActions.js b/huxley/www/js/actions/CurrentUserActions.js index c16686672..f34b09006 100644 --- a/huxley/www/js/actions/CurrentUserActions.js +++ b/huxley/www/js/actions/CurrentUserActions.js @@ -3,12 +3,12 @@ * Use of this source code is governed by a BSD License (see LICENSE). */ -'use strict'; +"use strict"; -var ActionConstants = require('constants/ActionConstants'); -var Dispatcher = require('dispatcher/Dispatcher'); +import ActionConstants from "constants/ActionConstants"; +import { Dispatcher } from "dispatcher/Dispatcher"; -var CurrentUserActions = { +const CurrentUserActions = { bootstrap() { Dispatcher.dispatch({ actionType: ActionConstants.BOOTSTRAP, @@ -48,4 +48,4 @@ var CurrentUserActions = { }, }; -module.exports = CurrentUserActions; +export { CurrentUserActions }; diff --git a/huxley/www/js/actions/DelegateActions.js b/huxley/www/js/actions/DelegateActions.js index 363f779c7..1d27ba386 100644 --- a/huxley/www/js/actions/DelegateActions.js +++ b/huxley/www/js/actions/DelegateActions.js @@ -3,10 +3,10 @@ * Use of this source code is governed by a BSD License (see LICENSE). */ -'use strict'; +"use strict"; -var ActionConstants = require('constants/ActionConstants'); -var Dispatcher = require('dispatcher/Dispatcher'); +import ActionConstants from "constants/ActionConstants"; +import { Dispatcher } from "dispatcher/Dispatcher"; var DelegateActions = { deleteDelegate(delegateID, onError) { @@ -61,4 +61,4 @@ var DelegateActions = { }, }; -module.exports = DelegateActions; +export { DelegateActions }; diff --git a/huxley/www/js/actions/NoteActions.js b/huxley/www/js/actions/NoteActions.js new file mode 100644 index 000000000..8eb72340e --- /dev/null +++ b/huxley/www/js/actions/NoteActions.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2011-2021 Berkeley Model United Nations. All rights reserved. + * Use of this source code is governed by a BSD License (see LICENSE). + */ + +'use strict'; + +import ActionConstants from 'constants/ActionConstants'; +import {Dispatcher} from 'dispatcher/Dispatcher'; + +var NoteActions = { + addNote(note) { + Dispatcher.dispatch({ + actionType: ActionConstants.ADD_NOTE, + note: note, + }); + }, + + notesFetched(notes) { + Dispatcher.dispatch({ + actionType: ActionConstants.NOTES_FETCHED, + notes: notes, + }); + }, +}; + +export { NoteActions }; diff --git a/huxley/www/js/actions/PositionPaperActions.js b/huxley/www/js/actions/PositionPaperActions.js index 27472fd31..66b13f6af 100644 --- a/huxley/www/js/actions/PositionPaperActions.js +++ b/huxley/www/js/actions/PositionPaperActions.js @@ -3,10 +3,10 @@ * Use of this source code is governed by a BSD License (see LICENSE). */ -'use strict'; +"use strict"; -var ActionConstants = require('constants/ActionConstants'); -var Dispatcher = require('dispatcher/Dispatcher'); +import ActionConstants from "constants/ActionConstants"; +import { Dispatcher } from "dispatcher/Dispatcher"; var PositionPaperActions = { fetchPositionPaperFile(paperID) { @@ -69,4 +69,4 @@ var PositionPaperActions = { }, }; -module.exports = PositionPaperActions; +export { PositionPaperActions }; diff --git a/huxley/www/js/actions/RegistrationActions.js b/huxley/www/js/actions/RegistrationActions.js index 97425d42b..4c495cc29 100644 --- a/huxley/www/js/actions/RegistrationActions.js +++ b/huxley/www/js/actions/RegistrationActions.js @@ -3,10 +3,10 @@ * Use of this source code is governed by a BSD License (see LICENSE). */ -'use strict'; +"use strict"; -var ActionConstants = require('constants/ActionConstants'); -var Dispatcher = require('dispatcher/Dispatcher'); +import ActionConstants from "constants/ActionConstants"; +import { Dispatcher } from "dispatcher/Dispatcher"; var RegistrationActions = { registrationFetched(registration) { @@ -26,4 +26,4 @@ var RegistrationActions = { }, }; -module.exports = RegistrationActions; +export { RegistrationActions }; diff --git a/huxley/www/js/actions/RubricActions.js b/huxley/www/js/actions/RubricActions.js index c47ef114f..c1917f359 100644 --- a/huxley/www/js/actions/RubricActions.js +++ b/huxley/www/js/actions/RubricActions.js @@ -3,10 +3,10 @@ * Use of this source code is governed by a BSD License (see LICENSE). */ -'use strict'; +"use strict"; -var ActionConstants = require('constants/ActionConstants'); -var Dispatcher = require('dispatcher/Dispatcher'); +import ActionConstants from "constants/ActionConstants"; +import { Dispatcher } from "dispatcher/Dispatcher"; var RubricActions = { updateRubric(rubric, onSuccess, onError) { @@ -26,4 +26,4 @@ var RubricActions = { }, }; -module.exports = RubricActions; +export { RubricActions }; diff --git a/huxley/www/js/actions/SecretariatMemberActions.js b/huxley/www/js/actions/SecretariatMemberActions.js index d99891449..d3d5d7654 100644 --- a/huxley/www/js/actions/SecretariatMemberActions.js +++ b/huxley/www/js/actions/SecretariatMemberActions.js @@ -3,10 +3,10 @@ * Use of this source code is governed by a BSD License (see LICENSE). */ -'use strict'; +"use strict"; -var ActionConstants = require('constants/ActionConstants'); -var Dispatcher = require('dispatcher/Dispatcher'); +import ActionConstants from "constants/ActionConstants"; +import { Dispatcher } from "dispatcher/Dispatcher"; var SecretariatMemberActions = { secretariatMembersFetched(secretariatMembers) { @@ -17,4 +17,4 @@ var SecretariatMemberActions = { }, }; -module.exports = SecretariatMemberActions; +export { SecretariatMemberActions }; diff --git a/huxley/www/js/components/AdvisorAssignmentsView.js b/huxley/www/js/components/AdvisorAssignmentsView.js index a77725219..675505e8b 100644 --- a/huxley/www/js/components/AdvisorAssignmentsView.js +++ b/huxley/www/js/components/AdvisorAssignmentsView.js @@ -3,50 +3,45 @@ * Use of this source code is governed by a BSD License (see LICENSE). */ -'use strict'; - -var React = require('react'); -var ReactRouter = require('react-router'); - -var _accessSafe = require('utils/_accessSafe'); -var AssignmentActions = require('actions/AssignmentActions'); -var AssignmentStore = require('stores/AssignmentStore'); -var Button = require('components/core/Button'); -var _checkDate = require('utils/_checkDate'); -var CommitteeStore = require('stores/CommitteeStore'); -var ConferenceContext = require('components/ConferenceContext'); -var CountryStore = require('stores/CountryStore'); -var CurrentUserStore = require('stores/CurrentUserStore'); -var CurrentUserActions = require('actions/CurrentUserActions'); -var DelegateActions = require('actions/DelegateActions'); -var DelegateSelect = require('components/DelegateSelect'); -var DelegateStore = require('stores/DelegateStore'); -var InnerView = require('components/InnerView'); -var RegistrationActions = require('actions/RegistrationActions'); -var RegistrationStore = require('stores/RegistrationStore'); -var ServerAPI = require('lib/ServerAPI'); -var Table = require('components/core/Table'); -var TextTemplate = require('components/core/TextTemplate'); - -var AdvisorAssignmentsViewText = require('text/AdvisorAssignmentsViewText.md'); -var AdvisorWaitlistText = require('text/AdvisorWaitlistText.md'); - -var AdvisorAssignmentsView = React.createClass({ - mixins: [ReactRouter.History], - - contextTypes: { - conference: React.PropTypes.shape(ConferenceContext), - }, - - getInitialState: function() { +"use strict"; + +import React from "react"; +import PropTypes from "prop-types"; + +var { _accessSafe } = require("utils/_accessSafe"); +var { AssignmentActions } = require("actions/AssignmentActions"); +var { AssignmentStore } = require("stores/AssignmentStore"); +var { Button } = require("components/core/Button"); +var { _checkDate } = require("utils/_checkDate"); +var { CommitteeStore } = require("stores/CommitteeStore"); +var { ConferenceContext } = require("components/ConferenceContext"); +var { CountryStore } = require("stores/CountryStore"); +var { CurrentUserStore } = require("stores/CurrentUserStore"); +var { CurrentUserActions } = require("actions/CurrentUserActions"); +var { DelegateActions } = require("actions/DelegateActions"); +var { DelegateSelect } = require("components/DelegateSelect"); +var { DelegateStore } = require("stores/DelegateStore"); +var { InnerView } = require("components/InnerView"); +var { RegistrationActions } = require("actions/RegistrationActions"); +var { RegistrationStore } = require("stores/RegistrationStore"); +var { ServerAPI } = require("lib/ServerAPI"); +var { Table } = require("components/core/Table"); +var { TextTemplate } = require("components/core/TextTemplate"); + +var AdvisorAssignmentsViewText = require("text/AdvisorAssignmentsViewText.md"); +var AdvisorWaitlistText = require("text/AdvisorWaitlistText.md"); + +class AdvisorAssignmentsView extends React.Component { + constructor(props) { + super(props); var schoolID = CurrentUserStore.getCurrentUser().school.id; var delegates = DelegateStore.getSchoolDelegates(schoolID); var assigned = this.prepareAssignedDelegates(delegates); - var conferenceID = this.context.conference.session; - return { + var conferenceID = global.conference.session; + this.state = { assigned: assigned, assignments: AssignmentStore.getSchoolAssignments(schoolID).filter( - assignment => !assignment.rejected, + (assignment) => !assignment.rejected ), committees: CommitteeStore.getCommittees(), countries: CountryStore.getCountries(), @@ -55,18 +50,18 @@ var AdvisorAssignmentsView = React.createClass({ success: false, registration: RegistrationStore.getRegistration(schoolID, conferenceID), }; - }, + } - componentDidMount: function() { + componentDidMount() { var schoolID = CurrentUserStore.getCurrentUser().school.id; - var conferenceID = this.context.conference.session; + var conferenceID = global.conference.session; this._committeesToken = CommitteeStore.addListener(() => { - this.setState({committees: CommitteeStore.getCommittees()}); + this.setState({ committees: CommitteeStore.getCommittees() }); }); this._countriesToken = CountryStore.addListener(() => { - this.setState({countries: CountryStore.getCountries()}); + this.setState({ countries: CountryStore.getCountries() }); }); this._delegatesToken = DelegateStore.addListener(() => { @@ -82,7 +77,7 @@ var AdvisorAssignmentsView = React.createClass({ this._assignmentsToken = AssignmentStore.addListener(() => { this.setState({ assignments: AssignmentStore.getSchoolAssignments(schoolID).filter( - assignment => !assignment.rejected, + (assignment) => !assignment.rejected ), }); }); @@ -92,29 +87,29 @@ var AdvisorAssignmentsView = React.createClass({ registration: RegistrationStore.getRegistration(schoolID, conferenceID), }); }); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { this._successTimout && clearTimeout(this._successTimeout); this._committeesToken && this._committeesToken.remove(); this._countriesToken && this._countriesToken.remove(); this._delegatesToken && this._delegatesToken.remove(); this._assignmentsToken && this._assignmentsToken.remove(); this._registrationToken && this._registrationToken.remove(); - }, + } - render: function() { + render() { var registration = this.state.registration; var waitlisted = - _accessSafe(registration, 'is_waitlisted') == null + _accessSafe(registration, "is_waitlisted") == null ? null : registration.is_waitlisted; var finalized = - _accessSafe(this.state.registration, 'assignments_finalized') == null + _accessSafe(this.state.registration, "assignments_finalized") == null ? false : this.state.registration.assignments_finalized; var committees = this.state.committees; - var conference = this.context.conference; + var conference = global.conference; var countries = this.state.countries; var shouldRenderAssignments = Object.keys(committees).length > 0 && @@ -126,8 +121,9 @@ var AdvisorAssignmentsView = React.createClass({ return ( + conferenceSession={global.conference.session} + conferenceExternal={global.conference.external} + > {AdvisorWaitlistText} @@ -135,19 +131,20 @@ var AdvisorAssignmentsView = React.createClass({ } else { return ( - + {AdvisorAssignmentsViewText} + isEmpty={!shouldRenderAssignments} + > - - + + @@ -158,28 +155,29 @@ var AdvisorAssignmentsView = React.createClass({ color="green" onClick={finalized ? this._handleSave : this._handleFinalize} loading={this.state.loading} - success={this.state.success}> - {finalized ? 'Save' : 'Finalize Assignments'} + success={this.state.success} + > + {finalized ? "Save" : "Finalize Assignments"} ); } - }, + } - renderAssignmentRows: function() { + renderAssignmentRows = () => { var committees = this.state.committees; var countries = this.state.countries; var finalized = - _accessSafe(this.state.registration, 'assignments_finalized') == null + _accessSafe(this.state.registration, "assignments_finalized") == null ? false : this.state.registration.assignments_finalized; return this.state.assignments.map( - function(assignment) { + function (assignment) { return ( - - - - + + + + ); - }.bind(this), + }.bind(this) ); - }, + }; /* To make it easier to assign and unassign delegates to assignments we maintain @@ -213,7 +212,7 @@ var AdvisorAssignmentsView = React.createClass({ to be assigned to it. This way we can easily manage the relationship from assignment to delegates via this object. */ - prepareAssignedDelegates: function(delegates) { + prepareAssignedDelegates = (delegates) => { var assigned = {}; for (var i = 0; i < delegates.length; i++) { if (delegates[i].assignment) { @@ -228,9 +227,9 @@ var AdvisorAssignmentsView = React.createClass({ } return assigned; - }, + }; - renderDelegateDropdown: function(assignment, slot) { + renderDelegateDropdown = (assignment, slot) => { var selectedDelegateID = assignment.id in this.state.assigned ? this.state.assigned[assignment.id][slot] @@ -242,16 +241,16 @@ var AdvisorAssignmentsView = React.createClass({ onChange={this._handleDelegateAssignment.bind( this, assignment.id, - slot, + slot )} delegates={this.state.delegates} selectedDelegateID={selectedDelegateID} disabled={disableView} /> ); - }, + }; - _handleDelegateAssignment: function(assignmentId, slot, event) { + _handleDelegateAssignment = (assignmentId, slot, event) => { var delegates = this.state.delegates; var assigned = this.state.assigned; var newDelegateId = event.target.value, @@ -281,11 +280,11 @@ var AdvisorAssignmentsView = React.createClass({ delegates: delegates, assigned: assigned, }); - }, + }; - _handleFinalize: function(event) { + _handleFinalize = (event) => { var confirm = window.confirm( - 'By pressing okay you are committing to the financial responsibility of each assignment. Are you sure you want to finalize assignments?', + "By pressing okay you are committing to the financial responsibility of each assignment. Are you sure you want to finalize assignments?" ); if (confirm) { RegistrationActions.updateRegistration( @@ -293,14 +292,14 @@ var AdvisorAssignmentsView = React.createClass({ { assignments_finalized: true, }, - this._handleError, + this._handleError ); } - }, + }; - _handleAssignmentDelete: function(assignment) { + _handleAssignmentDelete = (assignment) => { var confirm = window.confirm( - 'Are you sure you want to delete this assignment?', + "Are you sure you want to delete this assignment?" ); if (confirm) { AssignmentActions.updateAssignment( @@ -308,41 +307,41 @@ var AdvisorAssignmentsView = React.createClass({ { rejected: true, }, - this._handleError, + this._handleError ); } - }, + }; - _handleSave: function(event) { + _handleSave = (event) => { this._successTimout && clearTimeout(this._successTimeout); - this.setState({loading: true}); + this.setState({ loading: true }); var school = CurrentUserStore.getCurrentUser().school; DelegateActions.updateDelegates( school.id, this.state.delegates, this._handleSuccess, - this._handleError, + this._handleError ); - }, + }; - _handleSuccess: function(response) { + _handleSuccess = (response) => { this.setState({ loading: false, success: true, }); this._successTimeout = setTimeout( - () => this.setState({success: false}), - 2000, + () => this.setState({ success: false }), + 2000 ); - }, + }; - _handleError: function(response) { - this.setState({loading: false}); + _handleError = (response) => { + this.setState({ loading: false }); window.alert( - 'Something went wrong. Please refresh your page and try again.', + "Something went wrong. Please refresh your page and try again." ); - }, -}); + }; +} -module.exports = AdvisorAssignmentsView; +export { AdvisorAssignmentsView }; diff --git a/huxley/www/js/components/AdvisorFeedbackView.js b/huxley/www/js/components/AdvisorFeedbackView.js index da4955938..71b1e13ac 100644 --- a/huxley/www/js/components/AdvisorFeedbackView.js +++ b/huxley/www/js/components/AdvisorFeedbackView.js @@ -1,46 +1,39 @@ /** * Copyright (c) 2011-2015 Berkeley Model United Nations. All rights reserved. * Use of this source code is governed by a BSD License (see LICENSE). -*/ - -'use strict'; - -var React = require('react'); -var ReactRouter = require('react-router'); - -var _accessSafe = require('utils/_accessSafe'); -var AssignmentActions = require('actions/AssignmentActions'); -var AssignmentStore = require('stores/AssignmentStore'); -var Button = require('components/core/Button'); -var CommitteeStore = require('stores/CommitteeStore'); -var ConferenceContext = require('components/ConferenceContext'); -var CountryStore = require('stores/CountryStore'); -var CurrentUserStore = require('stores/CurrentUserStore'); -var DelegateStore = require('stores/DelegateStore'); -var InnerView = require('components/InnerView'); -var RegistrationStore = require('stores/RegistrationStore'); -var Table = require('components/core/Table'); -var TextTemplate = require('components/core/TextTemplate'); - -var AdvisorFeedbackViewText = require('text/AdvisorFeedbackViewText.md'); -var AdvisorWaitlistText = require('text/AdvisorWaitlistText.md'); - -var AdvisorFeedbackView = React.createClass({ - mixins: [ReactRouter.History], - - contextTypes: { - conference: React.PropTypes.shape(ConferenceContext), - }, - - getInitialState: function() { + */ + +"use strict"; + +import React from "react"; +import PropTypes from "prop-types"; + +var { _accessSafe } = require("utils/_accessSafe"); +var { AssignmentStore } = require("stores/AssignmentStore"); +var { CommitteeStore } = require("stores/CommitteeStore"); +var { ConferenceContext } = require("components/ConferenceContext"); +var { CountryStore } = require("stores/CountryStore"); +var { CurrentUserStore } = require("stores/CurrentUserStore"); +var { DelegateStore } = require("stores/DelegateStore"); +var { InnerView } = require("components/InnerView"); +var { RegistrationStore } = require("stores/RegistrationStore"); +var { Table } = require("components/core/Table"); +var { TextTemplate } = require("components/core/TextTemplate"); + +var AdvisorFeedbackViewText = require("text/AdvisorFeedbackViewText.md"); +var AdvisorWaitlistText = require("text/AdvisorWaitlistText.md"); + +class AdvisorFeedbackView extends React.Component { + constructor(props) { + super(props); var schoolID = CurrentUserStore.getCurrentUser().school.id; var delegates = DelegateStore.getSchoolDelegates(schoolID); - var conferenceID = this.context.conference.session; + var conferenceID = global.conference.session; var assignments = AssignmentStore.getSchoolAssignments(schoolID).filter( - assignment => !assignment.rejected, + (assignment) => !assignment.rejected ); var feedback = this.prepareFeedback(delegates); - return { + this.state = { registration: RegistrationStore.getRegistration(schoolID, conferenceID), feedback: feedback, assignments: assignments, @@ -49,11 +42,11 @@ var AdvisorFeedbackView = React.createClass({ delegates: delegates, loading: false, }; - }, + } - componentDidMount: function() { + componentDidMount() { var schoolID = CurrentUserStore.getCurrentUser().school.id; - var conferenceID = this.context.conference.session; + var conferenceID = global.conference.session; this._registrationToken = RegistrationStore.addListener(() => { this.setState({ registration: RegistrationStore.getRegistration(schoolID, conferenceID), @@ -61,17 +54,17 @@ var AdvisorFeedbackView = React.createClass({ }); this._committeesToken = CommitteeStore.addListener(() => { - this.setState({committees: CommitteeStore.getCommittees()}); + this.setState({ committees: CommitteeStore.getCommittees() }); }); this._countriesToken = CountryStore.addListener(() => { - this.setState({countries: CountryStore.getCountries()}); + this.setState({ countries: CountryStore.getCountries() }); }); this._assignmentsToken = AssignmentStore.addListener(() => { this.setState({ assignments: AssignmentStore.getSchoolAssignments(schoolID).filter( - assignment => !assignment.rejected, + (assignment) => !assignment.rejected ), }); }); @@ -84,28 +77,29 @@ var AdvisorFeedbackView = React.createClass({ feedback: feedback, }); }); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { this._registrationToken && this._registrationToken.remove(); this._committeesToken && this._committeesToken.remove(); this._countriesToken && this._countriesToken.remove(); this._delegatesToken && this._delegatesToken.remove(); this._assignmentsToken && this._assignmentsToken.remove(); - }, + } - render: function() { + render() { var registration = this.state.registration; var waitlisted = - _accessSafe(registration, 'is_waitlisted') == null + _accessSafe(registration, "is_waitlisted") == null ? null : registration.is_waitlisted; if (waitlisted) { return ( + conferenceSession={global.conference.session} + conferenceExternal={global.conference.external} + > {AdvisorWaitlistText} @@ -116,7 +110,8 @@ var AdvisorFeedbackView = React.createClass({ {AdvisorFeedbackViewText}
    Committee Country Delegation Size{finalized ? 'Delegate' : 'Delete Assignments'}{finalized ? 'Delegate' : ''}{finalized ? "Delegate" : "Delete Assignments"}{finalized ? "Delegate" : ""}
    {committees[assignment.committee].name}{countries[assignment.country].name}{committees[assignment.committee].delegation_size}
    {committees[assignment.committee] ? committees[assignment.committee].name : ''}{countries[assignment.country] ? countries[assignment.country].name : ''}{committees[assignment.committee] ? committees[assignment.committee].delegation_size : ''} {finalized ? ( this.renderDelegateDropdown(assignment, 0) @@ -187,7 +185,8 @@ var AdvisorAssignmentsView = React.createClass({ )} @@ -202,9 +201,9 @@ var AdvisorAssignmentsView = React.createClass({
    + isEmpty={!Object.keys(this.state.feedback).length} + > @@ -133,20 +128,20 @@ var AdvisorFeedbackView = React.createClass({ ); } - }, + } - renderAssignmentRows: function() { + renderAssignmentRows = () => { var assignments = this.state.assignments; var committees = this.state.committees; var countries = this.state.countries; var feedback = this.state.feedback; - return assignments.map(assignment => { + return assignments.map((assignment) => { var delegates = feedback[assignment.id]; - if (delegates == null) { + if (delegates == null || committees[assignment.committee] === undefined || countries[assignment.country] === undefined) { return; } return ( - +
    Committee
    {committees[assignment.committee].name} {countries[assignment.country].name} @@ -184,7 +179,7 @@ var AdvisorFeedbackView = React.createClass({