From 790b83910f20b0f26e29bff23192a7ea5b154484 Mon Sep 17 00:00:00 2001 From: Omkar Moghe Date: Tue, 11 Jul 2017 00:19:42 -0700 Subject: [PATCH] 'Massive refactor' - Manav 2017 --- MHacks/admin.py | 28 +- MHacks/announcements.py | 68 +++ MHacks/application_lists.py | 155 ----- MHacks/{v1/auth.py => authentication.py} | 39 +- MHacks/decorator.py | 32 - MHacks/events.py | 60 ++ MHacks/floors.py | 52 ++ MHacks/forms.py | 371 +----------- MHacks/frontend/__init__.py | 0 MHacks/frontend/views.py | 571 ------------------ MHacks/locations.py | 52 ++ MHacks/management/commands/scan_permission.py | 2 +- .../commands/send_push_notification.py | 7 +- MHacks/migrations/0001_initial.py | 4 +- MHacks/models.py | 367 +---------- MHacks/pass_creator.py | 69 --- ...ication_views.py => push_notifications.py} | 15 +- MHacks/scan_events.py | 193 ++++++ MHacks/tests.py | 2 - MHacks/{frontend => }/urls.py | 24 +- MHacks/users.py | 109 ++++ MHacks/utils.py | 203 +++++-- MHacks/v1/__init__.py | 0 MHacks/v1/announcements.py | 37 -- MHacks/v1/events.py | 26 - MHacks/v1/floors.py | 24 - MHacks/v1/locations.py | 23 - MHacks/v1/scan_event.py | 82 --- MHacks/v1/serializers/__init__.py | 6 - MHacks/v1/serializers/serializers.py | 108 ---- MHacks/v1/serializers/util.py | 72 --- MHacks/v1/urls.py | 39 -- MHacks/v1/util.py | 90 --- MHacks/v1/views.py | 196 ------ MHacks/v1_urls.py | 34 ++ MHacks/views.py | 228 +++++++ config/development_settings.py | 6 - config/settings.py | 8 +- config/urls.py | 7 +- requirements.txt | 2 +- static/javascript/announcements.js | 9 + 41 files changed, 1045 insertions(+), 2375 deletions(-) create mode 100644 MHacks/announcements.py delete mode 100644 MHacks/application_lists.py rename MHacks/{v1/auth.py => authentication.py} (62%) create mode 100644 MHacks/events.py create mode 100644 MHacks/floors.py delete mode 100644 MHacks/frontend/__init__.py delete mode 100644 MHacks/frontend/views.py create mode 100644 MHacks/locations.py delete mode 100644 MHacks/pass_creator.py rename MHacks/{v1/push_notification_views.py => push_notifications.py} (90%) create mode 100644 MHacks/scan_events.py rename MHacks/{frontend => }/urls.py (59%) create mode 100644 MHacks/users.py delete mode 100644 MHacks/v1/__init__.py delete mode 100644 MHacks/v1/announcements.py delete mode 100644 MHacks/v1/events.py delete mode 100644 MHacks/v1/floors.py delete mode 100644 MHacks/v1/locations.py delete mode 100644 MHacks/v1/scan_event.py delete mode 100644 MHacks/v1/serializers/__init__.py delete mode 100644 MHacks/v1/serializers/serializers.py delete mode 100644 MHacks/v1/serializers/util.py delete mode 100644 MHacks/v1/urls.py delete mode 100644 MHacks/v1/util.py delete mode 100644 MHacks/v1/views.py create mode 100644 MHacks/v1_urls.py diff --git a/MHacks/admin.py b/MHacks/admin.py index 2d28afd..23875cd 100644 --- a/MHacks/admin.py +++ b/MHacks/admin.py @@ -1,6 +1,11 @@ from django.contrib import admin -from models import * +from announcements import AnnouncementModel +from events import EventModel +from locations import LocationModel +from floors import FloorModel +from scan_events import ScanEventModel, ScanEventUser +from users import MHacksUser @admin.register(MHacksUser) @@ -8,12 +13,12 @@ class UserAdmin(admin.ModelAdmin): search_fields = ['first_name', 'last_name', 'email'] -@admin.register(Location) +@admin.register(LocationModel) class LocationAdmin(admin.ModelAdmin): search_fields = ['name'] -@admin.register(Event) +@admin.register(EventModel) class EventAdmin(admin.ModelAdmin): search_fields = ['name', 'info'] list_display = ['name', 'start', 'duration', 'category', 'deleted_string', 'location_list'] @@ -29,7 +34,7 @@ def location_list(self, obj): location_list.short_description = 'LOCATIONS' -@admin.register(Announcement) +@admin.register(AnnouncementModel) class AnnouncementAdmin(admin.ModelAdmin): search_fields = ['title', 'info'] list_display = ['title', 'broadcast_at', 'category', 'sent', 'approved', 'deleted_string'] @@ -40,18 +45,7 @@ def deleted_string(self, obj): deleted_string.short_description = 'DELETED' -@admin.register(Application) -class ApplicationAdmin(admin.ModelAdmin): - search_fields = ['user__first_name', 'user__last_name', 'user__email', 'decision'] - - -@admin.register(Registration) -class RegistrationAdmin(admin.ModelAdmin): - search_fields = ['user__first_name', 'user__last_name', 'user__email', 'acceptance', 'dietary_restrictions', - 'transportation'] - - -@admin.register(ScanEvent) +@admin.register(ScanEventModel) class ScanEventAdmin(admin.ModelAdmin): search_fields = ['name'] @@ -61,6 +55,6 @@ class ScanEventUserAdmin(admin.ModelAdmin): search_fields = ['scan_event__name'] -@admin.register(Floor) +@admin.register(FloorModel) class FloorAdmin(admin.ModelAdmin): search_fields = ['name'] diff --git a/MHacks/announcements.py b/MHacks/announcements.py new file mode 100644 index 0000000..22561a8 --- /dev/null +++ b/MHacks/announcements.py @@ -0,0 +1,68 @@ +from django.core.validators import MinValueValidator, MaxValueValidator +from django.db import models +from django.db.models import Q +from django.utils import timezone +from rest_framework.fields import CharField + +from utils import GenericListCreateModel, GenericUpdateDestroyModel, UnixEpochDateField +from models import Any, MHacksModelSerializer + + +class AnnouncementModel(Any): + title = models.CharField(max_length=60) + info = models.TextField(default='') + broadcast_at = models.DateTimeField() + category = models.PositiveIntegerField(validators=[MinValueValidator(0), + MaxValueValidator(31)], help_text="0 for none; 1 for emergency; 2 for logistics; 4 for food; 8 for event; Add 16 to make sponsored") + approved = models.BooleanField(default=False) + sent = models.BooleanField(default=False) + + @staticmethod + def max_category(): + return 31 + + def __unicode__(self): + return self.title + + +class AnnouncementSerializer(MHacksModelSerializer): + id = CharField(read_only=True) + broadcast_at = UnixEpochDateField() + + class Meta: + model = AnnouncementModel + fields = ('id', 'title', 'info', 'broadcast_at', 'category', 'approved') + + +class AnnouncementAPIView(GenericUpdateDestroyModel): + serializer_class = AnnouncementSerializer + queryset = AnnouncementModel.objects.all() + + +class AnnouncementListAPIView(GenericListCreateModel): + """ + Announcements are what send push notifications and are useful for pushing updates to MHacks participants. + Anybody who is logged in can make a GET request where as only authorized users can create, update and delete them. + """ + serializer_class = AnnouncementSerializer + query_set = AnnouncementModel.objects.none() + + def get_queryset(self): + date_last_updated = super(AnnouncementListAPIView, self).get_queryset() + + if not self.request.user or not self.request.user.has_perm('MHacks.change_announcement'): + query_set = AnnouncementModel.objects.all().filter(approved=True).filter(broadcast_at__lte=timezone.now()) + if date_last_updated: + query_set = query_set.filter(Q(last_updated__gte=date_last_updated) | Q(broadcast_at__gte=date_last_updated)) + else: + query_set = query_set.filter(deleted=False) + else: + query_set = AnnouncementModel.objects.all() + if date_last_updated: + query_set = query_set.filter(last_updated__gte=date_last_updated) + else: + query_set = query_set.filter(deleted=False) + return query_set + + + diff --git a/MHacks/application_lists.py b/MHacks/application_lists.py deleted file mode 100644 index 80d3bc8..0000000 --- a/MHacks/application_lists.py +++ /dev/null @@ -1,155 +0,0 @@ -# coding=utf-8 - -GENDER = [ - ('', 'Gender'), - ('male', 'Male'), - ('female', 'Female'), - ('non_binary', 'Non-Binary'), - ('other', 'Other') -] - -DEMOGRAPHIC_INFO = [ - ('', 'Race'), - ('american_indian_or_alaskan_native', 'American Indian or Alaskan Native'), - ('asian_or_pacific_islander', 'Asian or Pacific Islander'), - ('black', 'Black'), - ('hispanic', "Hispanic"), - ('white', 'White'), - ('other', 'Other') -] - -TECH_OPTIONS = [('ios', 'iOS'), - ('android', 'Android'), - ('web_dev', 'Web Dev'), - ('vr', 'Virtual/Augmented Reality'), - ('game_dev', 'Game Development'), - ('hardware', 'Hardware'), - ('ui_ux', 'Design'), - ('ai_ml', 'Artificial Intelligence/Machine Learning')] - -APPLICATION_DECISION = [ - 'Accept', - 'Waitlist', - 'Decline' -] - -USER_FOCUSED_DESIGN_SKILLS = [ - 'User experience research', - 'Interaction design', - 'Graphic design', - 'Product design', - 'Design thinking' -] - -SKILLS = [ - 'HTML/CSS', - 'Javascript', - 'Java', - 'C/C++', - 'PHP', - '3D modeling', - 'Breadboards', - 'Sensor electronics', - 'Haskell', - 'Pebble', - 'Swift', - 'Objective-C', - 'C#', - 'Arduino', - 'SPICE', - 'Oculus', - 'Android', - 'Node.js', - 'Ruby', - 'Python', - 'Unity', - 'Raspberry Pi', - 'Hardware design', - 'SQL' -] - -ACCEPTANCE = [ - ('accepted_yes', 'HAIL YEAH! 😀'), - ('accepted_no', 'No, I can\'t make it 😭') -] - -TRANSPORTATION = [ - ('plane', 'Plane ✈'), - ('train', 'Train 🚄'), - ('bus', 'Bus 🚌'), - ('personal', 'Personal transportation 🚘') -] - -T_SHIRT_SIZES = [ - 'S', - 'M', - 'L', - 'XL' -] - -DIETARY_RESTRICTIONS = [ - 'Vegan', - 'Vegetarian', - 'Gluten free', - 'Halal', - 'Kosher' -] - -DEGREES = [ - 'HS Diploma', - 'Bachelors', - 'Masters', - 'Doctorate' -] - -EMPLOYMENT = [ - ('internship', 'Internship'), - ('full time', 'Full Time Employment'), - ('part time', 'Part Time Employment while in school (Co-op)'), - ('none', 'Not Interested') -] - -EMPLOYMENT_SKILLS = [ - '.Net', - 'Android Development', - 'Arduino', - 'ASP', - 'Bash', - 'C', - 'C#', - 'C++', - 'CSS', - 'Data Testing', - 'Database experience', - 'Eclipse', - 'Hadoop', - 'Hive', - 'HTML', - 'iOS Development', - 'Java', - 'JavaScript', - 'JUnit', - 'LaTeX', - 'LINDO', - 'Linux Development', - 'Mac OS X Development', - 'MATLAB', - 'MySQL', - 'Node.js', - 'Objective C', - 'Oozie', - 'Perl', - 'PHP', - 'Python', - 'R', - 'Ruby', - 'SASS', - 'SQL', - 'Sqoop', - 'Swift', - 'Visual C++', - 'Web Development', - 'Windows Development' -] - - diff --git a/MHacks/v1/auth.py b/MHacks/authentication.py similarity index 62% rename from MHacks/v1/auth.py rename to MHacks/authentication.py index 6d1b0db..2388076 100644 --- a/MHacks/v1/auth.py +++ b/MHacks/authentication.py @@ -1,13 +1,15 @@ from django.views.decorators.csrf import csrf_exempt from push_notifications.models import APNSDevice, GCMDevice +from rest_framework import serializers +from rest_framework.authtoken.serializers import AuthTokenSerializer from rest_framework.response import Response from rest_framework.authtoken import views from rest_framework.authtoken.models import Token from rest_framework.authentication import SessionAuthentication -from MHacks.v1.serializers import AuthSerializer -from MHacks.v1.util import serialized_user +from announcements import AnnouncementModel as AnnouncementModel +from utils import serialized_user class CsrfExemptSessionAuthentication(SessionAuthentication): @@ -15,7 +17,7 @@ def enforce_csrf(self, request): return -class Authentication(views.ObtainAuthToken): +class AuthenticationAPIView(views.ObtainAuthToken): """ An easy convenient way to log a user in to get his/her token and the groups they are in. It also returns other basic information about the user like their name, etc. @@ -57,3 +59,34 @@ def post(self, request, *args, **kwargs): if push_notification: self.save_device(push_notification, user) return Response({'token': token.key, 'user': serialized_user(user)}) + + +class AuthSerializer(AuthTokenSerializer): + # Extends auth token serializer to accommodate push notifs + + token = serializers.CharField(required=False) + is_gcm = serializers.BooleanField(required=False) + + def validate(self, attributes): + attributes = super(AuthSerializer, self).validate(attributes) + + # Optionally add the token if it exists + if 'registration_id' in attributes.keys() and 'is_gcm' in attributes.keys(): + token = attributes.get('registration_id') + is_gcm = attributes.get('is_gcm') + preference = attributes.get('name', attributes.get('preference', '63')) + if not isinstance(preference, str): + preference = str(AnnouncementModel.max_category()) + attributes['push_notification'] = { + 'registration_id': token, + 'is_gcm': is_gcm, + 'name': preference + } + + return attributes + + def create(self, validated_data): + pass + + def update(self, instance, validated_data): + pass diff --git a/MHacks/decorator.py b/MHacks/decorator.py index 054d8c8..eee59ec 100644 --- a/MHacks/decorator.py +++ b/MHacks/decorator.py @@ -2,22 +2,6 @@ from django.shortcuts import resolve_url -def application_reader_required(view_function, redirect_to=None): - return ApplicationReaderRequired(view_function, redirect_to) - - -class ApplicationReaderRequired(object): - def __init__(self, view_function, redirect_to): - self.view_function = view_function - self.redirect_to = redirect_to - - def __call__(self, request, *args, **kwargs): - if request.user is None or not request.user.is_application_reader: - from django.conf import settings - return HttpResponseRedirect(resolve_url(self.redirect_to or settings.LOGIN_REDIRECT_URL)) - return self.view_function(request, *args, **kwargs) - - def anonymous_required(view_function, redirect_to=None): return AnonymousRequired(view_function, redirect_to) @@ -32,19 +16,3 @@ def __call__(self, request, *args, **kwargs): from django.conf import settings return HttpResponseRedirect(resolve_url(self.redirect_to or settings.LOGIN_REDIRECT_URL)) return self.view_function(request, *args, **kwargs) - - -def stats_team_required(view_function, redirect_to=None): - return StatsTeamRequired(view_function, redirect_to) - - -class StatsTeamRequired(object): - def __init__(self, view_function, redirect_to): - self.view_function = view_function - self.redirect_to = redirect_to - - def __call__(self, request, *args, **kwargs): - if not request.user or not request.user.groups.filter(name='stats_team').exists(): - from django.conf import settings - return HttpResponseRedirect(resolve_url(self.redirect_to or settings.LOGIN_REDIRECT_URL)) - return self.view_function(request, *args, **kwargs) \ No newline at end of file diff --git a/MHacks/events.py b/MHacks/events.py new file mode 100644 index 0000000..18989fd --- /dev/null +++ b/MHacks/events.py @@ -0,0 +1,60 @@ +from django.db import models +from rest_framework.fields import CharField, ChoiceField +from rest_framework.relations import PrimaryKeyRelatedField + +from utils import GenericListCreateModel, GenericUpdateDestroyModel, UnixEpochDateField, DurationInSecondsField +from locations import LocationModel +from models import Any, MHacksModelSerializer + + +class EventModel(Any): + name = models.CharField(max_length=60) + info = models.TextField(default='') + locations = models.ManyToManyField(LocationModel) + start = models.DateTimeField() + duration = models.DurationField() + CATEGORIES = ((0, 'General'), (1, 'Logistics'), + (2, 'Food'), (3, 'Learn'), (4, 'Social')) + category = models.IntegerField(choices=CATEGORIES) + approved = models.BooleanField(default=False) + + def __unicode__(self): + return self.name + + +class EventSerializer(MHacksModelSerializer): + id = CharField(read_only=True) + start = UnixEpochDateField() + locations = PrimaryKeyRelatedField(many=True, pk_field=CharField(), + queryset=LocationModel.objects.all().filter(deleted=False)) + duration = DurationInSecondsField() + category = ChoiceField(choices=EventModel.CATEGORIES) + + class Meta: + model = EventModel + fields = ('id', 'name', 'info', 'start', 'duration', + 'locations', 'category', 'approved') + + +class EventAPIView(GenericUpdateDestroyModel): + serializer_class = EventSerializer + queryset = EventModel.objects.all() + + +class EventListAPIView(GenericListCreateModel): + """ + Events are the objects that show up on the calendar view and represent + specific planned events at the hackathon. + """ + serializer_class = EventSerializer + query_set = EventModel.objects.none() + + def get_queryset(self): + date_last_updated = super(EventListAPIView, self).get_queryset() + if date_last_updated: + query_set = EventModel.objects.all().filter(last_updated__gte=date_last_updated) + else: + query_set = EventModel.objects.all().filter(deleted=False) + if not self.request.user or not self.request.user.has_perm('MHacks.change_event'): + return query_set.filter(approved=True) + return query_set diff --git a/MHacks/floors.py b/MHacks/floors.py new file mode 100644 index 0000000..8a47f66 --- /dev/null +++ b/MHacks/floors.py @@ -0,0 +1,52 @@ +from django.db import models +from rest_framework.fields import CharField + +from utils import GenericListCreateModel, GenericUpdateDestroyModel +from models import Any, MHacksModelSerializer + + +class FloorModel(Any): + name = models.CharField(max_length=60) + index = models.IntegerField(unique=True) + image = models.URLField() + description = models.TextField(blank=True) + offset_fraction = models.FloatField(default=1.0, null=True, blank=True) + aspect_ratio = models.FloatField(default=1.0, null=True, blank=True) + # Only used for maps ground overlay + nw_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + nw_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + se_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + se_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + + def __unicode__(self): + return '{} displayed at index {}'.format(self.name, str(self.index)) + + +class FloorSerializer(MHacksModelSerializer): + id = CharField(read_only=True) + + class Meta: + model = FloorModel + fields = ('id', 'name', 'image', 'index', 'offset_fraction', 'aspect_ratio', 'description', 'nw_latitude', + 'nw_longitude', 'se_latitude', 'se_longitude') + + +class FloorAPIView(GenericUpdateDestroyModel): + serializer_class = FloorSerializer + queryset = FloorModel.objects.all().filter(deleted=False) + + +class FloorListAPIView(GenericListCreateModel): + """ + Floors are the new map object + """ + serializer_class = FloorSerializer + query_set = FloorModel.objects.none() + + def get_queryset(self): + date_last_updated = super(FloorListAPIView, self).get_queryset() + if date_last_updated: + query_set = FloorModel.objects.all().filter(last_updated__gte=date_last_updated) + else: + query_set = FloorModel.objects.all().filter(deleted=False) + return query_set diff --git a/MHacks/forms.py b/MHacks/forms.py index 4c7731d..5186232 100644 --- a/MHacks/forms.py +++ b/MHacks/forms.py @@ -1,13 +1,9 @@ from django import forms from django.contrib.auth.forms import AuthenticationForm, UserCreationForm -from django.contrib.postgres.fields import ArrayField from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode -from django.utils.safestring import mark_safe -from MHacks.widgets import ArrayFieldSelectMultiple, MHacksAdminFileWidget -from models import MHacksUser, Application, MentorApplication, Registration -from utils import validate_url +from users import MHacksUser class LoginForm(AuthenticationForm): @@ -38,368 +34,3 @@ class Meta: model = MHacksUser fields = ('first_name', 'last_name', "email",) - -class ApplicationForm(forms.ModelForm): - def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user') - - super(ApplicationForm, self).__init__(*args, **kwargs) - - self.fields['is_high_school'].title = "General Information" - self.fields['gender'].title = 'Demographic Info' - self.fields['gender'].subtitle = '(Optional)' - self.fields['num_hackathons'].title = 'Previous Experience' - self.fields['cortex'].title = 'Interests' - self.fields['cortex'].subtitle = 'CTRL/CMD + click to multi-select!' - self.fields['passionate'].title = 'Short Answer' - self.fields['needs_reimbursement'].title = 'Travel' - - # self.fields['is_high_school'].full = True - # self.fields['is_international'].full = True - self.fields['mentoring'].full = True - # self.fields['needs_reimbursement'].full = True - self.fields['cortex'].full = True - self.fields['passionate'].full = True - self.fields['coolest_thing'].full = True - self.fields['other_info'].full = True - - self.fields['github'].required = False - self.fields['devpost'].required = False - self.fields['personal_website'].required = False - self.fields['other_info'].required = False - self.fields['other_links'].required = False - self.fields['gender'].required = False - self.fields['race'].required = False - self.fields['passionate'].required = True - - - # if the user is from UMich, exclude the reimbursement/travel fields - if self.user and 'umich.edu' in self.user.email: - for key in ['needs_reimbursement', 'from_city', 'from_state']: - del self.fields[key] - - - class Meta: - from application_lists import TECH_OPTIONS - model = Application - - # use all fields except for these - exclude = ['user', 'deleted', 'score', 'reimbursement', 'submitted', 'decision'] - - labels = { - 'school': 'University', - "grad_date": 'Expected graduation date', - 'birthday': 'Date of birth', - 'is_high_school': 'I am a high school student', - 'is_international': 'I am an international student', - - 'gender': 'Gender (optional)', - 'race': 'Race (optional)', - - 'num_hackathons': 'How many hackathons have you attended? (Put 0 if this is your first!)', - 'has_side_projects': 'Have you made anything outside of school (side projects, hacks, etc)?', - 'num_cs_courses': 'How many CS courses have you taken?', - 'num_ux_courses': 'How many UX/Design courses have you taken?', - 'github': 'GitHub', - 'linkedin': 'LinkedIn', - 'devpost': 'DevPost', - 'personal_website': 'Personal Website/Portfolio', - 'other_links': 'Anything else!', - 'resume': 'Resume (If you don\'t have a formal resume, you can upload a skills sheet, a bullet-pointed list, etc!)', - - - 'cortex': '', - - - 'passionate': 'Tell us about a project that you worked on and why you\'re proud of it. This doesn\'t have to be a hack! (150 words max)', - 'coolest_thing': 'What do you hope to take away from MHacks 9? (150 words max)', - 'other_info': 'Anything else you want to tell us?', - - - 'can_pay': 'How much of the travel cost can you pay?', - 'mentoring': 'I am interested in mentoring other hackers!', - 'needs_reimbursement': 'I will be needing travel reimbursement to attend MHacks.', - 'from_city': 'Which city will you be traveling from?', - 'from_state': 'Which state or country will you be traveling from? (Type your country if you are traveling internationally)', - } - - widgets = { - 'school': forms.TextInput(attrs={'placeholder': 'Hackathon College', 'class': 'form-control input-md', - 'id': 'school-autocomplete'}), - 'major': forms.TextInput(attrs={'placeholder': 'Hackathon Science', 'class': 'form-control input-md', - 'id': 'major-autocomplete'}), - "grad_date": forms.TextInput(attrs={'placeholder': 'MM/DD/YYYY', 'id': 'graduation_date'}), - 'birthday': forms.TextInput(attrs={'placeholder': 'MM/DD/YYYY'}), - # 'is_high_school': forms.RadioSelect(choices=((True, 'Yes'), (False, 'No'))), - # 'is_international': forms.RadioSelect(choices=((True, 'Yes'), (False, 'No'))), - - 'has_side_projects': forms.RadioSelect(choices=((True, 'Yes'), (False, 'No'))), - - 'github': forms.TextInput(attrs={'placeholder': 'https://github.com/username', 'class': 'form-control input-md'}), - 'linkedin': forms.TextInput(attrs={'placeholder': 'https://linkedin.com/in/username', 'class': 'form-control input-md'}), - 'devpost': forms.TextInput(attrs={'placeholder': 'https://devpost.com/username', 'class': 'form-control input-md'}), - 'personal_website': forms.TextInput(attrs={'placeholder': 'Personal Website', 'class': 'form-control input-md'}), - 'other_links': forms.TextInput(attrs={'placeholder': 'Other links', 'class': 'form-control input-md'}), - - 'resume': MHacksAdminFileWidget(attrs={'class': 'full form-control'}), - - 'cortex': ArrayFieldSelectMultiple(attrs={'class': 'checkbox-style check-width'}, choices=TECH_OPTIONS), - 'mentoring': forms.RadioSelect(choices=((True, 'Yes'), (False, 'No'))), - - 'passionate': forms.Textarea(attrs={'class': 'textfield form-control'}), - 'coolest_thing': forms.Textarea(attrs={'class': 'textfield form-control'}), - 'other_info': forms.Textarea(attrs={'class': 'textfield form-control'}), - - # 'needs_reimbursement': forms.RadioSelect(choices=((True, 'Yes'), (False, 'No'))), - 'from_city': forms.TextInput(attrs={'placeholder': 'City'}), - 'from_state': forms.TextInput(attrs={'placeholder': 'State or country', 'id': 'state-autocomplete'}) - - } - - # custom validator for urls - def clean_github(self): - data = self.cleaned_data['github'] - validate_url(data, 'github.com') - return data - - def clean_devpost(self): - data = self.cleaned_data['devpost'] - validate_url(data, 'devpost.com') - return data - - def clean_major(self): - data = self.cleaned_data['major'] - if not self.cleaned_data['is_high_school'] and not data: - raise forms.ValidationError('Please enter your major.') - return data - - def clean_grad_date(self): - data = self.cleaned_data['grad_date'] - if not self.cleaned_data['is_high_school'] and not data: - raise forms.ValidationError('Please enter your graduation date.') - - return data - - -class ApplicationSearchForm(forms.Form): - from application_lists import APPLICATION_DECISION - app_decisions = ['All'] + APPLICATION_DECISION - - # User related - first_name = forms.CharField(label='First name', max_length=255) - last_name = forms.CharField(label='Last name', max_length=255) - email = forms.CharField(label='Email', max_length=255) - - # Application - school = forms.CharField(label='School/College', max_length=255) - major = forms.CharField(label='Major', max_length=255) - gender = forms.CharField(label='Gender pronouns', max_length=255) - city = forms.CharField(label='From City', max_length=255) - state = forms.CharField(label='From State', max_length=255) - score_min = forms.IntegerField(label='Score Starts at') - score_max = forms.IntegerField(label='Score Ends at') - is_minor = forms.BooleanField(label='Minors') - is_veteran = forms.BooleanField(label='Veteran hackers') - is_beginner = forms.BooleanField(label='Beginner hackers') - is_non_UM = forms.BooleanField(label='Non-UMich hackers') - limit = forms.CharField(label='Number of results', max_length=255) - decision = forms.ChoiceField(label='Filter by decision', choices=zip(app_decisions, app_decisions)) - - -class SponsorPortalForm(forms.Form): - from application_lists import EMPLOYMENT, DEGREES, EMPLOYMENT_SKILLS - all_degrees = ['All'] + DEGREES - all_employment = [('All', 'All')] + EMPLOYMENT - all_employment_skills = ['All'] + EMPLOYMENT_SKILLS - - # User related - first_name = forms.CharField(label='First name', max_length=255) - last_name = forms.CharField(label='Last name', max_length=255) - email = forms.CharField(label='Email', max_length=255) - - # Registration related - education = forms.CharField(label='School or University', max_length=255) - employment = forms.ChoiceField(label='Type of employment', choices=all_employment) - degree = forms.ChoiceField(label='Type of degree', choices=zip(all_degrees, all_degrees)) - technical_skills = forms.ChoiceField(label='Filter by technical skills', choices=zip(all_employment_skills, all_employment_skills)) - - -class MentorApplicationForm(forms.ModelForm): - def __init__(self, *args, **kwargs): - super(MentorApplicationForm, self).__init__(*args, **kwargs) - - self.fields['agree_tc'].required = True - self.fields['user_focused_design_skills'].required = False - - self.fields['what_importance'].title = "Short Answer" - self.fields['skills'].title = "Skills" - self.fields['has_user_design_experience'].title = "User-Focused Design Skills" - self.fields['agree_tc'].title = "Commitment" - - self.fields['first_time_mentor'].full = True - self.fields['why_mentor'].full = True - self.fields['mentorship_ideas'].full = True - self.fields['what_importance'].full = True - self.fields['agree_tc'].full = True - - - class Meta: - from application_lists import SKILLS - from application_lists import USER_FOCUSED_DESIGN_SKILLS as DESIGN_SKILLS - - model = MentorApplication - - # use all fields except for these - exclude = ['user', 'submitted', 'deleted', 'score', 'reimbursement', 'decision'] - - labels = { - 'first_time_mentor': 'I am a first time mentor!', - 'what_importance': 'What do you think is important about being a mentor?', - 'why_mentor': 'Why do you want to be a mentor?', - 'mentorship_ideas': 'Do you have any ideas for mentorship at MHacks?', - 'has_user_design_experience': 'Do you have experience in user-focused design?', - 'user_focused_design_skills': 'If yes, which areas are you comfortable mentoring in? (CTRL/CMD + click to select multiple options!)', - 'skills': 'What skills are you comfortable mentoring in? (CTRL/CMD + click to select multiple options!)', - 'other_skills': 'Other skills', - 'github': 'GitHub', - 'agree_tc': 'I understand that by committing to mentor at MHacks 9 during the weekend of March 24-26, I will not work on my own project and will help participants to the best of my ability.' - } - - widgets = { - 'skills': ArrayFieldSelectMultiple(attrs={'class': 'full checkbox-style check-width'}, choices=zip(SKILLS, SKILLS)), - 'other_skills': forms.TextInput(attrs={'class': 'full check-width', 'placeholder': 'juggling...'}), - 'github': forms.TextInput(attrs={'class': 'full check-width', 'placeholder': '(optional)'}), - 'why_mentor': forms.Textarea(attrs={'class': 'textfield form-control'}), - 'mentorship_ideas': forms.Textarea(attrs={'class': 'textfield form-control'}), - 'what_importance': forms.Textarea(attrs={'class': 'textfield form-control'}), - 'has_user_design_experience': forms.RadioSelect(choices=((True, 'Yes'), (False, 'No'))), - 'user_focused_design_skills': ArrayFieldSelectMultiple(attrs={'class': 'full checkbox-style check-width'}, choices=zip(DESIGN_SKILLS, DESIGN_SKILLS)), - 'other_design_skills': forms.TextInput(attrs={'class': 'full check-width', 'placeholder': 'Dank meme design'}) - } - - - # custom validator for urls - def clean_github(self): - data = self.cleaned_data['github'] - validate_url(data, 'github.com') - return data - - def clean_agree_tc(self): - data = self.cleaned_data['agree_tc'] - if not data: - raise forms.ValidationError('You must agree to the terms & conditions to continue.') - return data - - -class RegistrationForm(forms.ModelForm): - def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user') - super(RegistrationForm, self).__init__(*args, **kwargs) - - self.fields['acceptance'].title = "Acceptance" - - self.fields['transportation'].title = "Logistics" - self.fields['transportation'].subtitle = "Note: checking an option does not guarantee travel reimbursement. All travel reimbursements are granted on a case-by-case basis. The amount you received for travel reimbursement was explicitly stated in the email notifying you of your application status and is also available on your Hacker Dashboard. If you have any questions, email us at hackathon@umich.edu. " - - self.fields['want_help'].title = "Mentorship" - - self.fields['t_shirt_size'].title = "Day-of Specifics" - - self.fields['employment'].title = "Sponsor & Employment Information" - self.fields['employment'].subtitle = "Sponsors will be able to sift through resumes based on the following data you provide. This is a great opportunity for you to showcase your resume to the world's top tech companies (most of whom are recruiting!). If you do not wish to have your resume looked at by our sponsors, please select 'Not Interested' in the following question" - - self.fields['code_of_conduct'].title = "Waivers and Code of Conduct" - self.fields['mlh_code_of_conduct'].title = "MLH Code Of Conduct" - - self.fields['code_of_conduct'].full = True - self.fields['waiver_signature'].full = True - self.fields['mlh_code_of_conduct'].full = True - self.fields['accommodations'].full = True - self.fields['medical_concerns'].full = True - self.fields['anything_else'].full = True - - # Don't ask umich students about fields - if self.user and 'umich.edu' in self.user.email: - del self.fields['transportation'] - - try: - hacker_app = Application.objects.get(user=self.user) - if not hacker_app.mentoring: - for key in ['can_help', 'other_can_help']: - del self.fields[key] - - if hacker_app.is_high_school: - self.fields['mlh_code_of_conduct'].subtitle = "If you are under the age of 18 you will be contacted with more liability forms that MUST be filled out and submitted before you attend the event in March." - except: - pass - - class Meta: - from application_lists import TECH_OPTIONS, EMPLOYMENT_SKILLS - model = Registration - - # use all fields except for these - exclude = ['user', 'submitted', 'deleted'] - - labels = { - 'acceptance': 'Do you accept your invitation to attend MHacks 9 this fall?', - 'transportation': 'How do you plan on getting to MHacks 9?', - 'want_help': 'What areas would you like to have help available in? (CTRL/CMD + click to select multiple options!)', - 'other_want_help': '', - 'can_help': 'What areas can you mentor another hacker in? (CTRL/CMD + click to select multiple options!)', - 'other_can_help': '', - 't_shirt_size': 'Please select your T-shirt size:', - 'dietary_restrictions': 'Please select any dietary restrictions:', - 'accommodations': 'Would you need any accommodations?', - 'medical_concerns': 'Do you have any medical concerns that we should be aware of?', - 'anything_else': 'Anything else we should know?', - 'phone_number': 'Please enter your phone number below:', - 'degree': 'What degree are you currently pursuing?', - 'employment': 'What types of employment are you interested in?', - 'technical_skills': 'Please select any technical skills you are competent in:', - 'code_of_conduct': mark_safe('I have read and agree to the terms of the MHacks Code of Conduct'), - 'waiver_signature': mark_safe('By signing below, I indicate my acceptance of the terms stated in the Accident Waiver and Release of Liability Form'), - 'mlh_code_of_conduct': mark_safe('
We participate in Major League Hacking (MLH) as a MLH Member Event. You authorize us to share certain application/registration information for event administration, ranking, MLH administration, pre and post-event informational e-mails, and occasional messages about hackathons in line with the MLH Privacy Policy.

I have read and agree to the terms of the MLH Code of Conduct') - } - - widgets = { - 'acceptance': forms.Select(attrs={'class': 'full checkbox-style'}), - 'transportation': forms.Select(attrs={'class': 'full checkbox-style'}), - 'want_help': ArrayFieldSelectMultiple(attrs={'class': 'full checkbox-style check-width'}, - choices=TECH_OPTIONS), - 'other_want_help': forms.TextInput(attrs={'class': 'full check-width', 'placeholder': 'Other areas'}), - 'can_help': ArrayFieldSelectMultiple(attrs={'class': 'full checkbox-style check-width'}, - choices=TECH_OPTIONS), - 'other_can_help': forms.TextInput(attrs={'class': 'check-width', 'placeholder': 'Other areas'}), - 't_shirt_size': forms.Select(attrs={'class': 'full checkbox-style'}), - 'dietary_restrictions': forms.Select(attrs={'class': 'full checkbox-style'}), - 'technical_skills': ArrayFieldSelectMultiple(attrs={'class': 'full checkbox-style check-width'}, - choices=zip(EMPLOYMENT_SKILLS, EMPLOYMENT_SKILLS)), - 'accommodations': forms.Textarea(attrs={'class': 'full textfield form-control', 'placeholder': '(e.g. wheelchair accessible transportation, closed captioning, etc.)'}), - 'medical_concerns': forms.Textarea(attrs={'class': 'full textfield form-control', 'placeholder': '(e.g. asthma, diabetes, epilepsy, etc.)'}), - 'anything_else': forms.Textarea(attrs={'class': 'full textfield form-control', 'placeholder': '(Your favorite joke...)'}), - 'phone_number': forms.TextInput(attrs={'class': 'full check-width', 'placeholder': ''}), - 'employment': forms.Select(attrs={'class': 'full checkbox-style'}), - 'degree': forms.Select(attrs={'class': 'full checkbox-style'}), - 'waiver_signature': forms.TextInput(attrs={'class': 'check-width', 'placeholder': 'First Last'}) - } - - def clean_waiver_signature(self): - data = self.cleaned_data['waiver_signature'] - user = self.user - if not data: - raise forms.ValidationError('You must sign the Accident Waiver and Release of Liability Form') - if not user.get_full_name().lower() == data.strip().lower(): - raise forms.ValidationError('Please sign your name as it appears in your user account: {}'.format(user.get_full_name())) - return data - - def clean_code_of_conduct(self): - data = self.cleaned_data['code_of_conduct'] - if not data: - raise forms.ValidationError('You must agree to the MHacks Code of Conduct.') - return data - - def clean_mlh_code_of_conduct(self): - data = self.cleaned_data['mlh_code_of_conduct'] - if not data: - raise forms.ValidationError('You must agree to the MLH Code of Conduct.') - return data diff --git a/MHacks/frontend/__init__.py b/MHacks/frontend/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/MHacks/frontend/views.py b/MHacks/frontend/views.py deleted file mode 100644 index 8361026..0000000 --- a/MHacks/frontend/views.py +++ /dev/null @@ -1,571 +0,0 @@ -import datetime -import os - -import mailchimp -from django.contrib.auth import login as auth_login, logout as auth_logout, get_user_model -from django.contrib.auth.decorators import login_required, permission_required, user_passes_test -from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm -from django.core.exceptions import ObjectDoesNotExist -from django.core.urlresolvers import reverse -from django.db.models import Q -from django.http import HttpResponse -from django.http import (HttpResponseBadRequest, HttpResponseNotAllowed, - HttpResponseForbidden) -from django.http import HttpResponseRedirect -from django.shortcuts import render, redirect -from django.utils.encoding import force_bytes, smart_str -from django.utils.http import urlsafe_base64_encode, is_safe_url -from django.views.decorators.csrf import csrf_exempt -from rest_framework.authtoken.models import Token - -from MHacks.decorator import anonymous_required, application_reader_required, stats_team_required -from MHacks.forms import RegisterForm, LoginForm, ApplicationForm, ApplicationSearchForm, RegistrationForm, \ - SponsorPortalForm, MentorApplicationForm -from MHacks.models import Application, MentorApplication, Registration -from MHacks.pass_creator import create_apple_pass -from MHacks.utils import send_verification_email, send_password_reset_email, validate_signed_token, \ - send_application_confirmation_email, send_registration_email -from config.settings import MAILCHIMP_API_KEY, LOGIN_REDIRECT_URL - -MAILCHIMP_API = mailchimp.Mailchimp(MAILCHIMP_API_KEY) - - -def blackout(request): - if request.method == 'POST': - if 'email' not in request.POST: - return HttpResponseBadRequest() - - email = request.POST.get("email") - list_id = "d9245d6d34" - try: - MAILCHIMP_API.lists.subscribe(list_id, {'email': email}, double_optin=False) - except mailchimp.ListAlreadySubscribedError: - return render(request, 'blackout.html', {'error': 'Looks like you\'re already subscribed!'}) - except Exception: - return render(request, 'blackout.html', { - 'error': 'Looks like there\'s been an error registering you. Try again or email us at hackathon@umich.edu'}) - return render(request, 'blackout.html', {'success': True}) - elif request.method == 'GET': - return render(request, 'blackout.html', {}) - else: - return HttpResponseNotAllowed(permitted_methods=['GET', 'POST']) - - -def index(request): - return render(request, 'index.html') - - -def thanks_registering(request): - return render(request, 'thanks_registering.html') - -@login_required() -@permission_required('MHacks.add_application') -@permission_required('MHacks.change_application') -def application(request): - - # find the user's application if it exists - try: - app = Application.objects.get(user=request.user, deleted=False) - except Application.DoesNotExist: - app = None - - if request.method == 'GET': - form = ApplicationForm(instance=app, user=request.user) - elif request.method == 'POST': - if not app: - try: - # look for deleted apps too - app = Application.objects.get(user=request.user) - except Application.DoesNotExist: - app = None - - form = ApplicationForm(data=request.POST, files=request.FILES, instance=app, user=request.user) - - if form.is_valid(): - # save application - app = form.save(commit=False) - app.user = request.user - app.submitted = True - app.deleted = False - send_application_confirmation_email(request.user) - - # save the app regardless - app.save() - - return redirect(reverse('mhacks-dashboard')) - else: - return HttpResponseNotAllowed(permitted_methods=['GET', 'POST']) - - context = {'form': form} - return render(request, 'application.html', context=context) - - -@login_required() -def apply_mentor(request): - # return redirect('https://docs.google.com/a/umich.edu/forms/d/e/1FAIpQLSdHtRqgaUORcwkwyOTkOZqDmcXGvPDmfZmEs2G13tbh9gzuBg/viewform') - - # unused as of 9/19/16 - try: - app = MentorApplication.objects.get(user=request.user, deleted=False) - except MentorApplication.DoesNotExist: - app = None - - if request.method == 'GET': - form = MentorApplicationForm(instance=app) - elif request.method == 'POST': - if not app: - try: - # look for deleted apps too - app = MentorApplication.objects.get(user=request.user) - except MentorApplication.DoesNotExist: - app = None - - form = MentorApplicationForm(data=request.POST, instance=app) - - if form.is_valid(): - # save application - app = form.save(commit=False) - app.user = request.user - app.submitted = True - app.deleted = False - app.save() - - return redirect(reverse('mhacks-dashboard')) - else: - return HttpResponseNotAllowed(permitted_methods=['GET', 'POST']) - - context = {'form': form} - return render(request, 'apply_mentor.html', context=context) - - -@login_required() -def registration(request): - # Changed to make this work for walk ons - # make sure the user is has submitted an application & has been accepted - # try: - # hacker_app = Application.objects.get(user=request.user) - # if not hacker_app.decision == 'Accept': - # return redirect(reverse('mhacks-dashboard')) - # except Application.DoesNotExist: - # return redirect(reverse('mhacks-dashboard')) - - # find the user's application if it exists - try: - app = Registration.objects.get(user=request.user, deleted=False) - - if app.submitted: - return redirect(reverse('mhacks-dashboard')) - except Registration.DoesNotExist: - app = None - - if request.method == 'GET': - form = RegistrationForm(instance=app, user=request.user) - elif request.method == 'POST': - if not app: - try: - # look for deleted apps too - app = Registration.objects.get(user=request.user) - except Registration.DoesNotExist: - app = None - - form = RegistrationForm(data=request.POST, instance=app, user=request.user) - - if form.is_valid(): - # save application - app = form.save(commit=False) - app.user = request.user - app.submitted = True - app.deleted = False - app.save() - send_registration_email(request.user, request) - return redirect(reverse('mhacks-dashboard')) - else: - return HttpResponseNotAllowed(permitted_methods=['GET', 'POST']) - - context = {'form': form} - return render(request, 'registration.html', context=context) - - -@anonymous_required -def login(request): - """ - A lot of this code is identical to the default login code but we have a few hooks (like username query) - and modifications so we implement it ourselves - """ - from django.contrib.auth.views import REDIRECT_FIELD_NAME - redirect_to = request.POST.get(REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME, '')) - if request.method == "POST": - form = LoginForm(request, data=request.POST) - if form.is_valid(): - # Ensure the user-originating redirection url is safe. - if not is_safe_url(url=redirect_to, host=request.get_host()): - redirect_to = reverse(LOGIN_REDIRECT_URL) - - # Okay, security check complete. Log the user in. - auth_login(request, form.get_user()) - - return redirect(redirect_to) - elif request.method == "GET": - form = LoginForm(request, initial=request.GET) - else: - return HttpResponseNotAllowed(permitted_methods=['GET', 'POST']) - context = { - 'form': form, - REDIRECT_FIELD_NAME: redirect_to, - } - return render(request, 'login.html', context) - - -def logout(request): - """ - We are nice about logout requests where if we are not logged in we fail silently. - However, we only accept POST requests for logout, not doing so is a security vulnerability, i.e. CSRF - attacks will be trivial (although not detrimental for security it can be bothersome to our users if malicious - users decide to use this attack) - """ - if request.method != 'POST': - return HttpResponseNotAllowed(permitted_methods=['POST']) - auth_logout(request) - return redirect(reverse('mhacks-home')) - - -@anonymous_required -def register(request): - user_pk = None - if request.method == 'POST': - form = RegisterForm(request.POST) - if form.is_valid(): - user = get_user_model().objects.create_user( - email=form.cleaned_data['email'], - password=form.cleaned_data['password1'], - first_name=form.cleaned_data['first_name'], - last_name=form.cleaned_data['last_name'], - request=request - ) - user.save() - user_pk = urlsafe_base64_encode(force_bytes(user.pk)) - form = None - return redirect(reverse('mhacks-thanks-registering')) - elif request.method == 'GET': - form = RegisterForm() - else: - return HttpResponseNotAllowed(permitted_methods=['POST', 'GET']) - return render(request, 'register.html', {'form': form, 'user_pk': user_pk}) - - -@anonymous_required -def request_verification_email(request, user_pk): - user = validate_signed_token(user_pk, None, require_token=False) - if user is not None: - send_verification_email(user, request) - return redirect(reverse('mhacks-login') + '?username=' + user.email) - - -# CSRF exempt because we need to allow a POST from mobile clients and marking this exempt cannot cause -# any security vulnerabilities since it is @anonymous_required -@csrf_exempt -@anonymous_required -def reset_password(request): - reset_type = 'reset_request' - if request.method == 'POST': - form = PasswordResetForm(request.POST) - if form.is_valid(): - try: - user = get_user_model().objects.get(email=form.cleaned_data["email"]) - except ObjectDoesNotExist: - form.errors['email'] = ["No user with that email exists"] - return render(request, 'password_reset.html', context={'form': form, 'type': reset_type}) - if user: - send_password_reset_email(user, request) - return redirect(reverse('mhacks-password_reset_sent')) - elif request.method == 'GET': - form = PasswordResetForm() - else: - return HttpResponseNotAllowed(permitted_methods=['GET', 'POST']) - if form: - form.fields['email'].longest = True - return render(request, 'password_reset.html', context={'form': form, 'type': reset_type}) - - -def password_reset_sent(request): - return render(request, 'password_reset_sent.html') - - -@anonymous_required -def validate_email(request, uid, token): - user = validate_signed_token(uid, token) - if user is None: - return HttpResponseForbidden() - user.email_verified = True - user.save() - return redirect(reverse('mhacks-login') + '?username=' + user.email) - - -@anonymous_required -def update_password(request, uid, token): - user = validate_signed_token(uid, token) - if not user: - return HttpResponseForbidden() # Just straight up forbid this request, looking fishy already! - if request.method == 'POST': - form = SetPasswordForm(user, data=request.POST) - if form.is_valid(): - form.save() - Token.objects.filter(user_id__exact=user.pk).delete() - return redirect(reverse('mhacks-login') + '?username=' + user.email) - elif request.method == 'GET': - form = SetPasswordForm(user) - else: - return HttpResponseNotAllowed(permitted_methods=['GET', 'POST']) - form.fields['new_password2'].label = 'Confirm New Password' - form.fields['new_password2'].longest = True - return render(request, 'password_reset.html', {'form': form, 'type': 'reset', 'uid': uid, 'token': token}) - - -@login_required -def dashboard(request): - if request.method == 'GET': - from MHacks.globals import groups - from MHacks.pass_creator import create_qr_code_image - app = request.user.application_or_none() - registration_app = request.user.registration_or_none() - qr_code = create_qr_code_image(request.user) - try: - mentor_app = MentorApplication.objects.get(user=request.user, deleted=False) - except MentorApplication.DoesNotExist: - mentor_app = None - - return render(request, 'dashboard.html', {'groups': groups, - 'application': app, - 'mentor_application': mentor_app, - 'registration_application': registration_app, - 'qr_code': qr_code}) - - return HttpResponseNotAllowed(permitted_methods=['GET']) - - -@login_required -@application_reader_required -def application_search(request): - if request.method == 'GET': - form = ApplicationSearchForm() - context = {'form': form} - return render(request, 'application_search.html', context=context) - - return HttpResponseNotAllowed(permitted_methods=['GET']) - - -@login_required -@application_reader_required -def application_review(request): - if request.method == 'GET': - event_date = datetime.date(1998, 10, 7) - - search_dict = {} - - hacker_search_keys = { - 'first_name': ['user__first_name', 'istartswith'], - 'last_name': ['user__last_name', 'istartswith'], - 'email': ['user__email', 'icontains'], - 'school': ['school', 'icontains'], - 'major': ['major', 'icontains'], - 'gender': ['gender', 'icontains'], - 'city': ['from_city', 'icontains'], - 'state': ['from_state', 'icontains'], - 'score_min': ['score', 'gte'], - 'score_max': ['score', 'lte'], - } - - mentor_search_keys = { - 'first_name': ['user__first_name', 'istartswith'], - 'last_name': ['user__last_name', 'istartswith'], - 'email': ['user__email', 'icontains'] - } - - # pick search dict based on which type of search - search_keys = dict() - if 'hacker' in request.GET: - search_keys = hacker_search_keys - elif 'mentor' in request.GET: - search_keys = mentor_search_keys - - for key in search_keys: - if request.GET.get(key): - condition = "{0}__{1}".format(search_keys[key][0], search_keys[key][1]) - search_dict[condition] = request.GET[key] - - # get the types of applications based on which type of search - applications = Application.objects.none() - if 'hacker' in request.GET: - applications = Application.objects.filter(**search_dict) - - if request.GET.get('is_veteran'): - applications = applications.filter(num_hackathons__gt=1) - - if request.GET.get('is_beginner'): - applications = applications.filter(num_hackathons__lt=2) - elif 'mentor' in request.GET: - applications = MentorApplication.objects.filter(**search_dict) - - # submitted applications - applications = applications.filter(submitted=True) - - if request.GET.get('is_non_UM'): - applications = applications.filter(~Q(user__email__icontains='umich.edu')) - - if request.GET.get('is_minor'): - applications = applications.filter(birthday__gte=event_date) - - if request.GET.get('decision') and not request.GET.get('decision') == 'All': - applications = applications.filter(decision=request.GET.get('decision')) - - # from the oldest applicants - applications = applications.order_by('last_updated') - - if request.GET.get('limit'): - applications = applications if (int(request.GET['limit']) > len(applications)) else applications[:int( - request.GET['limit'])] - - applications = applications.filter(deleted=False) - context = {'results': applications} - # return the appropriate HTML view - if 'hacker' in request.GET: - return render(request, 'application_review.html', context=context) - elif 'mentor' in request.GET: - return render(request, 'mentor_review.html', context=context) - - return HttpResponseNotAllowed(permitted_methods=['GET']) - - -@login_required -@application_reader_required -def update_applications(request): - if request.method == 'POST': - id_list = request.POST.getlist('id[]') - score_list = request.POST.getlist('score[]') - decision_list = request.POST.getlist('decision[]') - reimbursement_list = request.POST.getlist('reimbursement[]') - - for i in range(len(id_list)): - # negative check - reimbursement_amount = float(reimbursement_list[i]) - reimbursement_amount = reimbursement_amount if reimbursement_amount >= 0 else 0 - - if request.POST.get('application_type') == 'hacker': - Application.objects.filter(id=id_list[i]).update(score=score_list[i], - decision=decision_list[i], - reimbursement=reimbursement_amount) - elif request.POST.get('application_type') == 'mentor': - MentorApplication.objects.filter(id=id_list[i]).update(score=score_list[i], - decision=decision_list[i], - reimbursement=reimbursement_amount) - - return HttpResponseRedirect(request.META.get('HTTP_REFERER')) - - -@login_required -@stats_team_required -def stats(request): - return render(request, 'stats.html') - - -def live(request): - # Server time is one hour behind - current_hour = (datetime.datetime.now().hour + 1) % 24; - - print current_hour - - return render(request, 'live.html', {'hour': current_hour}) - - -@login_required() -def apple_pass(request): - response = HttpResponse(content=create_apple_pass(request.user).getvalue(), content_type='application/vnd.apple.pkpass') - response['MimeType'] = 'application/vnd.apple.pkpass' - return response - - -@user_passes_test(lambda u: u.groups.filter(name='sponsor').exists()) -def sponsor_portal(request): - if request.method == 'GET': - form = SponsorPortalForm() - context = {'form': form} - return render(request, 'sponsor_portal.html', context=context) - - return HttpResponseNotAllowed(permitted_methods=['GET']) - - -@user_passes_test(lambda u: u.groups.filter(name='sponsor').exists()) -def sponsor_review(request): - if request.method == 'GET': - search_dict = dict() - search_keys = { - 'first_name': ['user__first_name', 'istartswith'], - 'last_name': ['user__last_name', 'istartswith'], - 'email': ['user__email', 'icontains'], - 'employment': ['employment', 'icontains'], - 'degree': ['degree', 'icontains'], - 'technical_skills': ['technical_skills', 'icontains'] - } - - for key in search_keys: - if request.GET.get(key): - if key in ['employment', 'degree', 'technical_skills'] and request.GET.get(key) == 'All': - continue - else: - condition = "{0}__{1}".format(search_keys[key][0], search_keys[key][1]) - search_dict[condition] = request.GET[key] - - results = list() - registrations = Registration.objects.filter(**search_dict) - for reg in registrations: - try: - hacker_app = Application.objects.get(user=reg.user) - except Application.DoesNotExist: - hacker_app = None - - results.append((reg, hacker_app)) - - if request.GET.get('education'): - results = [r for r in results if r[1] and request.GET.get('education') in r[1].school.lower()] - - context = { - 'results': results - } - - return render(request, 'sponsor_review.html', context=context) - - return HttpResponseNotAllowed(permitted_methods=['GET']) - - -@user_passes_test(lambda u: u.is_superuser or u.groups.filter(name='sponsor').exists() or u.groups.filter(name='application_reader').exists()) -def resumes(request, filename): - from config.settings import DEBUG - if not DEBUG: - from django_boto.s3.storage import S3Storage - - storage = S3Storage() - - if storage.exists(filename) and Application.objects.filter(resume=filename).exists(): - app = Application.objects.get(resume=filename) - file_ending = filename.split('.')[-1] - file = storage.open(filename) - file.seek(0) - response = HttpResponse(content=file.read(), - content_type='application/force-download') - response['Content-Disposition'] = 'attachment; filename=%s' % smart_str(app.user.first_name + " " + app.user.last_name + "." + file_ending) - return response - else: - from config.settings import MEDIA_ROOT - - path = os.path.join(MEDIA_ROOT, filename) - if os.path.isfile(path) and Application.objects.filter(resume=filename).exists(): - app = Application.objects.get(resume=filename) - file_ending = filename.split('.')[-1] - response = HttpResponse(content=open(path, "rb"), - content_type='application/force-download') - response['Content-Disposition'] = 'attachment; filename=%s' % smart_str(app.user.first_name + " " + app.user.last_name + "." + file_ending) - response['X-Sendfile'] = smart_str(path) - response['Content-Length'] = os.path.getsize(path) - return response - - return HttpResponseRedirect(request.META.get('HTTP_REFERER')) diff --git a/MHacks/locations.py b/MHacks/locations.py new file mode 100644 index 0000000..994ca72 --- /dev/null +++ b/MHacks/locations.py @@ -0,0 +1,52 @@ +from django.db import models +from rest_framework.fields import CharField + +from utils import GenericListCreateModel, GenericUpdateDestroyModel, NonNullPrimaryKeyField +from floors import FloorModel +from models import Any, MHacksModelSerializer + + +class LocationModel(Any): + name = models.CharField(max_length=60) + floor = models.ForeignKey( + FloorModel, on_delete=models.PROTECT, null=True, blank=True) + latitude = models.DecimalField( + max_digits=9, decimal_places=6, null=True, blank=True) + longitude = models.DecimalField( + max_digits=9, decimal_places=6, null=True, blank=True) + + def __unicode__(self): + if self.floor: + return "{} on the {}".format(self.name, str(self.floor.name)) + return self.name + + +class LocationSerializer(MHacksModelSerializer): + id = CharField(read_only=True) + floor = NonNullPrimaryKeyField(many=False, pk_field=CharField(), + queryset=FloorModel.objects.all().filter(deleted=False), + allow_empty=True) + + class Meta: + model = LocationModel + fields = ('id', 'name', 'floor', 'latitude', 'longitude') + + +class LocationAPIView(GenericUpdateDestroyModel): + serializer_class = LocationSerializer + queryset = LocationModel.objects.all() + + +class LocationListAPIView(GenericListCreateModel): + """ + Locations are specific important locations that can be linked to from other tables. + """ + serializer_class = LocationSerializer + query_set = LocationModel.objects.none() + + def get_queryset(self): + date_last_updated = super(LocationListAPIView, self).get_queryset() + if date_last_updated: + return LocationModel.objects.all().filter(last_updated__gte=date_last_updated) + else: + return LocationModel.objects.all().filter(deleted=False) diff --git a/MHacks/management/commands/scan_permission.py b/MHacks/management/commands/scan_permission.py index 047cbf2..1c2e23a 100644 --- a/MHacks/management/commands/scan_permission.py +++ b/MHacks/management/commands/scan_permission.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand from django.contrib.auth.models import Permission -from MHacks.models import MHacksUser +from users import MHacksUser class Command(BaseCommand): diff --git a/MHacks/management/commands/send_push_notification.py b/MHacks/management/commands/send_push_notification.py index 4396eff..f9c99b9 100644 --- a/MHacks/management/commands/send_push_notification.py +++ b/MHacks/management/commands/send_push_notification.py @@ -1,7 +1,8 @@ from django.core.management.base import BaseCommand -from push_notifications.models import APNSDevice, GCMDevice from push_notifications.apns import APNSDataOverflow, apns_send_bulk_message -from MHacks.models import Announcement +from push_notifications.models import APNSDevice, GCMDevice + +from announcements import AnnouncementModel class Command(BaseCommand): @@ -10,7 +11,7 @@ class Command(BaseCommand): def handle(self, *args, **options): import pytz from datetime import datetime - announcements = Announcement.objects.all().filter(sent=False, approved=True, broadcast_at__lte=datetime.now(pytz.utc)) + announcements = AnnouncementModel.objects.all().filter(sent=False, approved=True, broadcast_at__lte=datetime.now(pytz.utc)) for announcement in announcements: announcement.sent = True announcement.save() # Save immediately so even if this takes time to run, we won't have duplicate pushes diff --git a/MHacks/migrations/0001_initial.py b/MHacks/migrations/0001_initial.py index 22ebc55..3c30f78 100644 --- a/MHacks/migrations/0001_initial.py +++ b/MHacks/migrations/0001_initial.py @@ -9,6 +9,8 @@ from django.db import migrations, models import django.db.models.deletion +import users + class Migration(migrations.Migration): @@ -133,7 +135,7 @@ class Migration(migrations.Migration): 'verbose_name': 'User', }, managers=[ - ('objects', MHacks.models.MHacksUserManager()), + ('objects', users.MHacksUserManager()), ], ), migrations.AddField( diff --git a/MHacks/models.py b/MHacks/models.py index 1ad4a8e..4b9c4d5 100644 --- a/MHacks/models.py +++ b/MHacks/models.py @@ -1,117 +1,12 @@ # coding=utf-8 from __future__ import unicode_literals -from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager -from django.contrib.postgres.fields import ArrayField -from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator from django.db import models +from rest_framework.serializers import ModelSerializer -from globals import GroupEnum -from config.settings import AUTH_USER_MODEL from managers import MHacksQuerySet -class MHacksUserManager(BaseUserManager): - use_in_migrations = True - - def _create_user(self, email, password, first_name, last_name, **extra_fields): - """ - Creates and saves a User with the given email and password. - """ - if not email: - raise ValueError('The given email must be set') - email = self.normalize_email(email) - if not self.model: - self.model = MHacksUser - try: - request = extra_fields.pop('request') - except KeyError: - request = None - user = self.model(email=email, first_name=first_name, last_name=last_name, **extra_fields) - user.set_password(password) - user.save(using=self._db) - from django.contrib.auth.models import Group - user.groups.add(Group.objects.get(name=GroupEnum.HACKER)) - user.save(using=self._db) - from utils import send_verification_email - if request: - send_verification_email(user, request) - return user - - def create_user(self, email, password, first_name, last_name, **extra_fields): - extra_fields.setdefault('is_superuser', False) - return self._create_user(email, password, first_name, last_name, **extra_fields) - - def create_superuser(self, email, password, first_name, last_name, **extra_fields): - extra_fields.setdefault('is_superuser', True) - if extra_fields.get('is_superuser') is not True: - raise ValueError('Superuser must have is_superuser=True.') - return self._create_user(email, password, first_name, last_name, **extra_fields) - - -class MHacksUser(AbstractBaseUser, PermissionsMixin): - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=30) - email = models.EmailField(unique=True, db_index=True) - email_verified = models.BooleanField(default=False) - - objects = MHacksUserManager() - - USERNAME_FIELD = 'email' - REQUIRED_FIELDS = ['first_name', 'last_name'] - - def application_or_none(self): - try: - return Application.objects.get(user=self, deleted=False) - except Application.DoesNotExist: - return None - - def registration_or_none(self): - try: - return Registration.objects.get(user=self, deleted=False) - except Registration.DoesNotExist: - return None - - def cleaned_school_name(self, application=None): - if not application: - application = self.application_or_none() - return application.school.replace('-', ' – ') if application else 'Unknown' - - class Meta: - verbose_name = 'User' - default_permissions = () - - @property - def is_active(self): - return self.email_verified - - @property - def is_staff(self): - return self.is_superuser - - @property - def is_sponsor(self): - return self.groups.filter(name='sponsor').exists() - - @property - def is_application_reader(self): - return self.groups.filter(name='application_reader').exists() - - def get_full_name(self): - """ - Returns the first_name plus the last_name, with a space in between. - """ - full_name = '%s %s' % (self.first_name, self.last_name) - return full_name.strip() - - def get_short_name(self): - """Returns the short name for the user.""" - return self.first_name - - def __unicode__(self): - return self.get_full_name() - - class Any(models.Model): last_updated = models.DateTimeField(auto_now=True) created = models.DateTimeField(auto_now_add=True) @@ -130,257 +25,9 @@ class Meta: abstract = True -class Floor(Any): - name = models.CharField(max_length=60) - index = models.IntegerField(unique=True) - image = models.URLField() - description = models.TextField(blank=True) - offset_fraction = models.FloatField(default=1.0, null=True, blank=True) - aspect_ratio = models.FloatField(default=1.0, null=True, blank=True) - # Only used for maps ground overlay - nw_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) - nw_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) - se_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) - se_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) - - def __unicode__(self): - return '{} displayed at index {}'.format(self.name, str(self.index)) - - -class Location(Any): - name = models.CharField(max_length=60) - floor = models.ForeignKey(Floor, on_delete=models.PROTECT, null=True, blank=True) - latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) - longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) - - def __unicode__(self): - if self.floor: - return "{} on the {}".format(self.name, str(self.floor.name)) - return self.name - - -class Event(Any): - name = models.CharField(max_length=60) - info = models.TextField(default='') - locations = models.ManyToManyField(Location) - start = models.DateTimeField() - duration = models.DurationField() - CATEGORIES = ((0, 'General'), (1, 'Logistics'), (2, 'Food'), (3, 'Learn'), (4, 'Social')) - category = models.IntegerField(choices=CATEGORIES) - approved = models.BooleanField(default=False) - - def __unicode__(self): - return self.name - - -class Announcement(Any): - title = models.CharField(max_length=60) - info = models.TextField(default='') - broadcast_at = models.DateTimeField() - category = models.PositiveIntegerField(validators=[MinValueValidator(0), - MaxValueValidator(31)], help_text="0 for none; 1 for emergency; 2 for logistics; 4 for food; 8 for event; Add 16 to make sponsored") - approved = models.BooleanField(default=False) - sent = models.BooleanField(default=False) - - @staticmethod - def max_category(): - return 31 - - def __unicode__(self): - return self.title - - -class Application(Any): - from application_lists import TECH_OPTIONS, APPLICATION_DECISION, GENDER, DEMOGRAPHIC_INFO - - # General information - user = models.OneToOneField(AUTH_USER_MODEL) - is_high_school = models.BooleanField(default=False) - is_international = models.BooleanField(default=False) - school = models.CharField(max_length=255, default='') - major = models.CharField(max_length=255, default='', blank=True) - grad_date = models.DateField(null=True, blank=True) - birthday = models.DateField() - - # both demographic info and gender are optional - # Demographic - gender = models.CharField(choices=GENDER, max_length=64, default='') - race = models.CharField(choices=DEMOGRAPHIC_INFO, max_length=64, default='') - - # Previous Experience - num_hackathons = models.IntegerField(default=0, validators=[ - MinValueValidator(limit_value=0, message='You went to negative hackathons? Weird...')]) - has_side_projects = models.BooleanField(default=False) - num_cs_courses = models.IntegerField(default=0) - num_ux_courses = models.IntegerField(default=0) - - # External Links - github = models.URLField(default='https://github.com/username') - linkedin = models.URLField(default='https://linkedin.com/in/username') - devpost = models.URLField(default='https://devpost.com/username') - personal_website = models.URLField(default='') - other_links = models.URLField(default='') - - from utils import change_resume_filename - from config.settings import DEBUG - - if not DEBUG: - from django_boto.s3.storage import S3Storage - - s3_storage = S3Storage() - resume = models.FileField(max_length=(10 * 1024 * 1024), upload_to=change_resume_filename, storage=s3_storage) # 10 MB max file size - else: - resume = models.FileField(max_length=(10 * 1024 * 1024), upload_to=change_resume_filename) # 10 MB max file size - - # Interests - cortex = ArrayField(models.CharField(max_length=16, choices=TECH_OPTIONS, default='', blank=True), - size=len(TECH_OPTIONS)) - mentoring = models.BooleanField(default=False) - - # Short Answer - passionate = models.TextField() - coolest_thing = models.TextField() - other_info = models.TextField() - - # Logistics - needs_reimbursement = models.BooleanField(default=False) - can_pay = models.FloatField(default=0, validators=[MinValueValidator(limit_value=0.0)]) - from_city = models.CharField(max_length=255, default='') - from_state = models.CharField(max_length=64, default='') - - # Miscellaneous - submitted = models.BooleanField(default=False) - - # Private administrative use - score = models.FloatField(default=0) - reimbursement = models.FloatField(default=0, validators=[MinValueValidator(limit_value=0.0)]) - decision = models.CharField(max_length=16, choices=zip(APPLICATION_DECISION, APPLICATION_DECISION), - default='Decline') - - def __unicode__(self): - return self.user.get_full_name() + '\'s Application' - - def user_is_minor(self): - from datetime import date - return self.birthday >= date(year=1999, month=03, day=24) - - -class MentorApplication(Any): - from application_lists import SKILLS, APPLICATION_DECISION, USER_FOCUSED_DESIGN_SKILLS - - user = models.OneToOneField(AUTH_USER_MODEL) - - # Mentor Info - first_time_mentor = models.BooleanField(default=False) - - # Short Response - what_importance = models.TextField() - why_mentor = models.TextField() - mentorship_ideas = models.TextField() - - # User-Focused Design Skills Review - has_user_design_experience = models.BooleanField(default=False) - user_focused_design_skills = ArrayField( - models.CharField( - max_length=32, - choices=zip(USER_FOCUSED_DESIGN_SKILLS, USER_FOCUSED_DESIGN_SKILLS), - blank=True), - size=len(USER_FOCUSED_DESIGN_SKILLS), - blank=True, - null=True - ) - other_design_skills = models.CharField(max_length=255, default='', blank=True) - - # Skill Review - skills = ArrayField(models.CharField(max_length=32, choices=zip(SKILLS, SKILLS), blank=True), size=len(SKILLS)) - other_skills = models.CharField(max_length=255, default='', blank=True) - github = models.URLField(blank=True) - - # Commitment - agree_tc = models.BooleanField(default=False) - - # Internal - submitted = models.BooleanField(default=False) - score = models.FloatField(default=0) - reimbursement = models.FloatField(default=0, validators=[MinValueValidator(limit_value=0.0)]) - decision = models.CharField(max_length=16, choices=zip(APPLICATION_DECISION, APPLICATION_DECISION), - default='Decline') - - def __unicode__(self): - return self.user.get_full_name() + '\'s Mentor Application' - - -class Registration(Any): - from application_lists import ACCEPTANCE, TRANSPORTATION, TECH_OPTIONS, T_SHIRT_SIZES, DIETARY_RESTRICTIONS, \ - DEGREES, EMPLOYMENT, EMPLOYMENT_SKILLS - - # User - user = models.OneToOneField(AUTH_USER_MODEL) - - # Acceptance - acceptance = models.CharField(max_length=32, choices=ACCEPTANCE) - - # Logistics - transportation = models.CharField(max_length=32, choices=TRANSPORTATION) - - # Mentorship - want_help = ArrayField(models.CharField(max_length=16, choices=TECH_OPTIONS, blank=True), size=len(TECH_OPTIONS), - blank=True) - other_want_help = models.CharField(max_length=64, blank=True) - can_help = ArrayField(models.CharField(max_length=16, choices=TECH_OPTIONS, blank=True), size=len(TECH_OPTIONS), - blank=True, null=True) - other_can_help = models.CharField(max_length=64, blank=True) - - # Day-of Specifics - t_shirt_size = models.CharField(max_length=4, choices=zip(T_SHIRT_SIZES, T_SHIRT_SIZES)) - dietary_restrictions = models.CharField(max_length=32, choices=zip(DIETARY_RESTRICTIONS, DIETARY_RESTRICTIONS), - blank=True) - accommodations = models.TextField(blank=True) - medical_concerns = models.TextField(blank=True) - anything_else = models.TextField(blank=True) - phone_number = models.CharField(max_length=16, - validators=[RegexValidator(regex=r'^\+?1?\d{9,15}$', - message="Phone number must be entered in the format: \ - '+#########'. Up to 15 digits allowed.")]) - - # Sponsor & Employment Information - employment = models.CharField(max_length=64, choices=EMPLOYMENT) - degree = models.CharField(max_length=16, choices=zip(DEGREES, DEGREES)) - technical_skills = ArrayField( - models.CharField(max_length=32, choices=zip(EMPLOYMENT_SKILLS, EMPLOYMENT_SKILLS), blank=True), - size=len(EMPLOYMENT_SKILLS), blank=True) - - # Waivers and Code of Conduct - code_of_conduct = models.BooleanField(default=False) - waiver_signature = models.CharField(max_length=128) - mlh_code_of_conduct = models.BooleanField(default=False) - - # Internal - submitted = models.BooleanField(default=False) - - def __unicode__(self): - return self.user.get_full_name() + '\'s Registration' - - -class ScanEvent(Any): - name = models.CharField(max_length=60, unique=True) - number_of_allowable_scans = models.IntegerField(default=1, validators=[MinValueValidator(limit_value=0)]) - scanned_users = models.ManyToManyField(AUTH_USER_MODEL, related_name="scan_event_users", blank=True, through='ScanEventUser') - expiry_date = models.DateTimeField(blank=True, null=True) - custom_verification = models.CharField(blank=True, max_length=255) - - class Meta: - permissions = (("can_perform_scan", "Can perform a scan"),) - - def __unicode__(self): - return self.name - - -# A custom proxy used for the Many To Many field below -class ScanEventUser(models.Model): - scan_event = models.ForeignKey(ScanEvent, on_delete=models.CASCADE) - user = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.CASCADE) - count = models.IntegerField(default=1) - - def __unicode__(self): - return self.user.get_full_name() + '\'s ' + self.scan_event.name + ' Scan (' + str(self.count) + ')' +class MHacksModelSerializer(ModelSerializer): + def to_representation(self, instance): + if getattr(instance, 'deleted', False): + # noinspection PyProtectedMember + return {instance._meta.pk.name: str(instance.pk), 'deleted': True} + return super(MHacksModelSerializer, self).to_representation(instance) diff --git a/MHacks/pass_creator.py b/MHacks/pass_creator.py deleted file mode 100644 index 1d73ed8..0000000 --- a/MHacks/pass_creator.py +++ /dev/null @@ -1,69 +0,0 @@ -from config.settings import APPLE_WALLET_PASSPHRASE, STATICFILES_DIRS -from passbook.models import Pass, Barcode, EventTicket, BarcodeFormat, Alignment, Field, IBeacon -import qrcode - - -def create_apple_pass(user): - card_info = EventTicket() - header_field = Field('date', 'March 24-26') - header_field.textAlignment = Alignment.RIGHT - card_info.headerFields.append(header_field) - card_info.addPrimaryField('name', user.get_full_name(), 'HACKER') - card_info.addBackField('name', user.get_full_name(), 'NAME') - card_info.addBackField('email', user.email, 'EMAIL') - - app = user.application_or_none() - - school_name = user.cleaned_school_name(app) - card_info.addSecondaryField('school', school_name, 'SCHOOL') - card_info.addBackField('school', school_name, 'SCHOOL') - - if app: - if app.user_is_minor(): - card_info.addAuxiliaryField('minor', 'YES', 'MINOR') - card_info.addBackField('minor', 'YES', 'MINOR') - - registration = user.registration_or_none() - if registration: - card_info.addBackField('tshirt', registration.t_shirt_size, 'T-SHIRT SIZE') - card_info.addBackField('dietary', registration.dietary_restrictions if registration.dietary_restrictions else 'None', 'DIETARY RESTRICTIONS') - - pass_file = Pass(card_info, passTypeIdentifier='pass.com.MPowered.MHacks.UserPass', - organizationName='MHacks', teamIdentifier='478C74MJ7T') - pass_file.description = 'MHacks Ticket' - pass_file.serialNumber = str(user.pk) - pass_file.barcode = Barcode(message=user.email, format=BarcodeFormat.QR) - pass_file.backgroundColor = 'rgb(241, 103, 88)' - pass_file.foregroundColor = 'rgb(250, 250, 250)' - pass_file.labelColor = 'rgba(0, 0, 0, 0.6)' - pass_file.associatedStoreIdentifiers = [955659359] - - pass_file.locations = [{'longitude': 42.3415958, 'latitude': -83.0606906}, - {'longitude': 42.3420320, 'latitude': -83.0596780}, - {'longitude': 42.3415800, 'latitude': -83.0607620}] - i_beacon = IBeacon('5759985C-B037-43B4-939D-D6286CE9C941', 0, 0) - i_beacon.relevantText = 'You are near a scanner.' - pass_file.ibeacons = [i_beacon] - - # Including the icon is necessary for the passbook to be valid. - pass_file.addFile('icon.png', open(STATICFILES_DIRS[0] + '/assets/app_icon.png', 'r')) - pass_file.addFile('icon@2x.png', open(STATICFILES_DIRS[0] + '/assets/app_icon.png', 'r')) - pass_file.addFile('icon@3x.png', open(STATICFILES_DIRS[0] + '/assets/app_icon.png', 'r')) - pass_file.addFile('logo.png', open(STATICFILES_DIRS[0] + '/assets/apple_pass_logo.png', 'r')) - pass_file.addFile('logo@2x.png', open(STATICFILES_DIRS[0] + '/assets/apple_pass_logo.png', 'r')) - pass_file.addFile('logo@3x.png', open(STATICFILES_DIRS[0] + '/assets/apple_pass_logo.png', 'r')) - - # Create and return the Passbook file (.pkpass) - return pass_file.create('config/apple_wallet_certificate.pem', - 'config/apple_wallet_key.pem', - 'config/apple_wallet_wwdr.pem', - APPLE_WALLET_PASSPHRASE) - - -def create_qr_code_image(user): - import StringIO - import base64 - output = StringIO.StringIO() - image = qrcode.make(user.email) - image.save(output) - return base64.b64encode(output.getvalue()) diff --git a/MHacks/v1/push_notification_views.py b/MHacks/push_notifications.py similarity index 90% rename from MHacks/v1/push_notification_views.py rename to MHacks/push_notifications.py index 9c05bc8..1d186c7 100644 --- a/MHacks/v1/push_notification_views.py +++ b/MHacks/push_notifications.py @@ -1,10 +1,11 @@ +from push_notifications.api.rest_framework import APNSDeviceSerializer, GCMDeviceSerializer +from push_notifications.models import APNSDevice, GCMDevice from rest_framework import generics, status +from rest_framework.exceptions import ValidationError from rest_framework.permissions import AllowAny from rest_framework.response import Response -from rest_framework.exceptions import ValidationError -from push_notifications.api.rest_framework import APNSDeviceSerializer, GCMDeviceSerializer -from push_notifications.models import APNSDevice, GCMDevice -from MHacks.models import Announcement + +from announcements import AnnouncementModel class PushNotificationView(generics.CreateAPIView): @@ -14,13 +15,13 @@ class PushNotificationView(generics.CreateAPIView): def create(self, request, *args, **kwargs): preference = request.data.get('preference', 0) if not preference: - preference = request.data.get('name', str(Announcement.max_category())) + preference = request.data.get('name', str(AnnouncementModel.max_category())) try: if int(preference) <= 0: - preference = str(Announcement.max_category()) + preference = str(AnnouncementModel.max_category()) except ValueError: - preference = str(Announcement.max_category()) + preference = str(AnnouncementModel.max_category()) copied_data = request.data.copy() copied_data['name'] = preference diff --git a/MHacks/scan_events.py b/MHacks/scan_events.py new file mode 100644 index 0000000..eeeee3d --- /dev/null +++ b/MHacks/scan_events.py @@ -0,0 +1,193 @@ +from django.contrib.auth import get_user_model +from django.core.validators import MinValueValidator +from django.db import models +from rest_framework.decorators import api_view, permission_classes +from rest_framework.exceptions import ValidationError +from rest_framework.fields import CharField +from rest_framework.permissions import BasePermission +from rest_framework.response import Response + +from utils import GenericListCreateModel, GenericUpdateDestroyModel, UnixEpochDateField, now_as_utc_epoch, to_utc_epoch +from models import Any, MHacksModelSerializer +from settings import AUTH_USER_MODEL + + +class ScanEventModel(Any): + name = models.CharField(max_length=60, unique=True) + number_of_allowable_scans = models.IntegerField(default=1, validators=[MinValueValidator(limit_value=0)]) + scanned_users = models.ManyToManyField(AUTH_USER_MODEL, related_name="scan_event_users", blank=True, through='ScanEventUser') + expiry_date = models.DateTimeField(blank=True, null=True) + custom_verification = models.CharField(blank=True, max_length=255) + + class Meta: + permissions = (("can_perform_scan", "Can perform a scan"),) + + def __unicode__(self): + return self.name + + +class ScanEventSerializer(MHacksModelSerializer): + id = CharField(read_only=True) + expiry_date = UnixEpochDateField() + + class Meta: + model = ScanEventModel + fields = ('id', 'name', 'expiry_date') + + +# A custom proxy used for the Many To Many field below +class ScanEventUser(models.Model): + scan_event = models.ForeignKey(ScanEventModel, on_delete=models.CASCADE) + user = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.CASCADE) + count = models.IntegerField(default=1) + + def __unicode__(self): + return self.user.get_full_name() + '\'s ' + self.scan_event.name + ' Scan (' + str(self.count) + ')' + + +class ScanEventAPIView(GenericUpdateDestroyModel): + serializer_class = ScanEventSerializer + queryset = ScanEventModel.objects.all() + + +class ScanEventListAPIView(GenericListCreateModel): + """ + Announcements are what send push notifications and are useful for pushing updates to MHacks participants. + Anybody who is logged in can make a GET request where as only authorized users can create, update and delete them. + """ + serializer_class = ScanEventSerializer + query_set = ScanEventModel.objects.none() + + def get_queryset(self): + date_last_updated = super(ScanEventListAPIView, self).get_queryset() + if date_last_updated: + query_set = ScanEventModel.objects.all().filter(last_updated__gte=date_last_updated) + else: + query_set = ScanEventModel.objects.all().filter(deleted=False) + + return query_set + + +class CanPerformScan(BasePermission): + def has_permission(self, request, view): + return request.user and request.user.is_authenticated and request.user.has_perm('MHacks.can_perform_scan') + + +@api_view(http_method_names=['POST', 'GET']) +@permission_classes((CanPerformScan,)) +def perform_scan(request): + from scan_events import error_field + if request.method == 'POST': + information = request.POST + else: + information = request.GET + + scan_event_id = information.get('scan_event', None) + user_id = information.get('user_id', None) + if not scan_event_id or not user_id: + raise ValidationError('Invalid fields provided') + + try: + scan_event = ScanEventModel.objects.get(pk=scan_event_id) + user = get_user_model().objects.get(email=user_id) + except (ScanEventModel.DoesNotExist, get_user_model().DoesNotExist): + raise ValidationError('Invalid scan event or user') + + if scan_event.expiry_date: + if scan_event.deleted or to_utc_epoch(scan_event.expiry_date) < now_as_utc_epoch(): + raise ValidationError('Scan event is no longer valid') + + successful_scan = True + error = None + data = [] + scan_event_user_join = None + if scan_event.number_of_allowable_scans: + try: + scan_event_user_join = ScanEventUser.objects.get(user=user, scan_event=scan_event) + number_of_scans = scan_event_user_join.count + except (ScanEventUser.DoesNotExist, get_user_model().DoesNotExist): + number_of_scans = 0 + + if number_of_scans >= scan_event.number_of_allowable_scans: + successful_scan = False + error = error_field('Can\'t scan again') + + success = True + if scan_event.custom_verification: + try: + # FIXME: Figure out what to do here... + success, data = getattr(name=scan_event.custom_verification)(request, user) + except AttributeError: + pass # This shouldn't happen normally but we defensively protect against it + successful_scan = successful_scan and success + if error: + data.append(error) + scan_result = {'scanned': successful_scan, 'data': data} + + # Only if its a POST request do we actually "do" the scan + # GET requests are peeks i.e. they don't modify the database at all + # If there is no number_of_allowable_scans we don't do anything on a POST either (unlimited) + if successful_scan and scan_event.number_of_allowable_scans and request.method == 'POST': + if scan_event_user_join: + scan_event_user_join.count += 1 + else: + scan_event_user_join = ScanEventUser(user=user, scan_event=scan_event, count=1) + scan_event_user_join.save() + + return Response(data=scan_result) + + +# Scan Verifiers +# IMPORTANT NOTE: All scan verifiers must go in this file as a global function, if not +# it will simply not work + +def registration_scan_verify(request, scanned_user): + succeeded = True + application = scanned_user.application_or_none() + registration = scanned_user.registration_or_none() + all_fields = _general_information(scanned_user, application) + if application and application.user_is_minor(): + all_fields.append(_create_field('MINOR', 'Yes', color='FF0000')) + + if not registration or not registration.acceptance: + all_fields.append(error_field('Not registered. Send to registration desk.')) + succeeded = False + return succeeded, all_fields + + +def general_information_scan_verify(request, scanned_user): + return True, _general_information(scanned_user) + + +def swag_scan_verify(request, scanned_user): + succeeded = True + all_fields = [_create_field('NAME', scanned_user.get_full_name())] + registration = scanned_user.registration_or_none() + if not registration: + all_fields.append(error_field('Not registered. Send to registration desk.')) + succeeded = False + else: + all_fields.append(_create_field('T-SHIRT SIZE', registration.t_shirt_size)) + application = scanned_user.application_or_none() + if application and application.mentoring: + all_fields.append(_create_field('MENTOR', 'Yes', color='0000FF')) + + return succeeded, all_fields + + +def _general_information(user, application=None): + return [_create_field('NAME', user.get_full_name()), + _create_field('EMAIL', user.email), + _create_field('SCHOOL', user.cleaned_school_name(application))] + + +def error_field(value): + return _create_field('ERROR', value, color='FF0000') + + +def _create_field(label, value, color='000000'): + return {'label': label, + 'value': value, + 'color': color} + + diff --git a/MHacks/tests.py b/MHacks/tests.py index 7ce503c..a39b155 100644 --- a/MHacks/tests.py +++ b/MHacks/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/MHacks/frontend/urls.py b/MHacks/urls.py similarity index 59% rename from MHacks/frontend/urls.py rename to MHacks/urls.py index 48613e1..76d8a22 100644 --- a/MHacks/frontend/urls.py +++ b/MHacks/urls.py @@ -1,7 +1,9 @@ from django.conf.urls import url, include -from MHacks.frontend.views import * from django.views.generic.base import RedirectView +from MHacks.views import * +from views import index + urlpatterns = [ # Blackout # url(r'^$', blackout, name='mhacks-blackout'), @@ -25,30 +27,12 @@ # Content url(r'^dashboard/$', dashboard, name='mhacks-dashboard'), - url(r'^live/$', live, name='mhacks-live'), - url(r'^apply/$', application, name='mhacks-apply'), url(r'^thanks_for_registering/$', thanks_registering, name='mhacks-thanks-registering'), - url(r'^applyMentor/$', apply_mentor, name='mhacks-applyMentor'), - url(r'^registration/$', registration, name='mhacks-registration'), - - # Application reading - url(r'^application_search/$', application_search, name='mhacks-applicationSearch'), - url(r'^application_review/$', application_review, name='mhacks-applicationReview'), - url(r'^update_applications/$', update_applications, name='mhacks-updateApplication'), - - # Statistics (logistical info for orgianizers) - url(r'^stats/$', stats, name='mhacks-stats'), - - # Sponsor portal - url(r'^sponsor_portal', sponsor_portal, name='mhacks-sponsorPortal'), - url(r'^sponsor_review', sponsor_review, name='mhacks-sponsorReview'), - url(r'^resumes/(?P[\w\S]{0,256})/$', resumes, name='mhacks-resumes'), # Admin only url(r'^explorer/', include('explorer.urls')), - # Apple Wallet pass support - url(r'^apple_pass.pkpass$', apple_pass, name='mhacks-apple-pass'), + url(r'^apple-app-site-association', apple_site_association), # Redirect all other endpoints to the homepage url(r'^.*/$', RedirectView.as_view(url='/', permanent=False), name='redirect-mhacks-home') diff --git a/MHacks/users.py b/MHacks/users.py new file mode 100644 index 0000000..156924d --- /dev/null +++ b/MHacks/users.py @@ -0,0 +1,109 @@ +from django.contrib.auth.base_user import BaseUserManager, AbstractBaseUser +from django.contrib.auth.models import PermissionsMixin +from django.db import models +from rest_framework.decorators import api_view, permission_classes +from rest_framework.fields import CharField +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from globals import GroupEnum +from utils import serialized_user +from models import MHacksModelSerializer + + +class MHacksUserManager(BaseUserManager): + use_in_migrations = True + + def _create_user(self, email, password, first_name, last_name, **extra_fields): + """ + Creates and saves a User with the given email and password. + """ + if not email: + raise ValueError('The given email must be set') + email = self.normalize_email(email) + if not self.model: + self.model = MHacksUser + try: + request = extra_fields.pop('request') + except KeyError: + request = None + user = self.model(email=email, first_name=first_name, last_name=last_name, **extra_fields) + user.set_password(password) + user.save(using=self._db) + from django.contrib.auth.models import Group + user.groups.add(Group.objects.get(name=GroupEnum.HACKER)) + user.save(using=self._db) + from utils import send_verification_email + if request: + send_verification_email(user, request) + return user + + def create_user(self, email, password, first_name, last_name, **extra_fields): + extra_fields.setdefault('is_superuser', False) + return self._create_user(email, password, first_name, last_name, **extra_fields) + + def create_superuser(self, email, password, first_name, last_name, **extra_fields): + extra_fields.setdefault('is_superuser', True) + if extra_fields.get('is_superuser') is not True: + raise ValueError('Superuser must have is_superuser=True.') + return self._create_user(email, password, first_name, last_name, **extra_fields) + + +class MHacksUser(AbstractBaseUser, PermissionsMixin): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + email = models.EmailField(unique=True, db_index=True) + email_verified = models.BooleanField(default=False) + + objects = MHacksUserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['first_name', 'last_name'] + + class Meta: + verbose_name = 'User' + default_permissions = () + + @property + def is_active(self): + return self.email_verified + + @property + def is_staff(self): + return self.is_superuser + + @property + def is_sponsor(self): + return self.groups.filter(name='sponsor').exists() + + @property + def is_application_reader(self): + return self.groups.filter(name='application_reader').exists() + + def get_full_name(self): + """ + Returns the first_name plus the last_name, with a space in between. + """ + full_name = '%s %s' % (self.first_name, self.last_name) + return full_name.strip() + + def get_short_name(self): + """Returns the short name for the user.""" + return self.first_name + + def __unicode__(self): + return self.get_full_name() + + +@api_view(http_method_names=['GET']) +@permission_classes((IsAuthenticated,)) +def update_user_profile(request): + return Response(data=serialized_user(request.user)) + + +class MHacksUserSerializer(MHacksModelSerializer): + id = CharField(read_only=True) + + class Meta: + model = MHacksUser + fields = ('id', 'first_name', 'last_name', 'email') diff --git a/MHacks/utils.py b/MHacks/utils.py index 053cde1..7a74e6d 100644 --- a/MHacks/utils.py +++ b/MHacks/utils.py @@ -1,22 +1,24 @@ -import sys import logging +import sys import mandrill from django import forms from django.contrib.auth import get_user_model from django.contrib.auth.tokens import default_token_generator from django.contrib.staticfiles.storage import staticfiles_storage -from django.core.mail import send_mail from django.core.urlresolvers import reverse -from django.template import loader from django.utils.encoding import force_bytes, force_text from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from jinja2 import Environment - -from config.settings import EMAIL_HOST_USER -from config.settings import MANDRILL_API_KEY +from rest_framework import serializers +from rest_framework.generics import CreateAPIView, ListAPIView, RetrieveUpdateDestroyAPIView +from rest_framework.permissions import IsAuthenticatedOrReadOnly, DjangoModelPermissions +from rest_framework.response import Response +from rest_framework.status import HTTP_201_CREATED +from rest_framework.views import exception_handler from MHacks.globals import permissions_map +from config.settings import MANDRILL_API_KEY # Updates permissions to groups @@ -138,33 +140,6 @@ def send_password_reset_email(user, request): ) -def send_registration_email(user, request=None): - from pass_creator import create_qr_code_image - from pass_creator import create_apple_pass - import base64 - if request: - wallet_url = _get_absolute_url(request, reverse('mhacks-apple-pass')) - else: - wallet_url = "{0}://{1}{2}".format('https', 'mhacks.org', reverse('mhacks-apple-pass')) - send_mandrill_mail('ticket_email_simple', 'Your MHacks Ticket', user.email, - email_vars={ - 'FIRST_NAME': user.get_short_name(), - 'WALLET_URL': wallet_url, - 'QR_CODE': "cid:qrcode.png", - 'FULL_NAME': user.get_full_name(), - 'SCHOOL': user.cleaned_school_name() - }, - attachments=[{'content': base64.b64encode(create_apple_pass(user).getvalue()), - 'name': 'mhacks.pkpass', - 'type': 'application/vnd.apple.pkpass' - }], - images=[{'content': create_qr_code_image(user), - 'name': 'qrcode.png', - 'type': 'image/png' - }] - ) - - def validate_signed_token(uid, token, require_token=True): """ Validates a signed token and uid and returns the user who owns it. @@ -219,16 +194,154 @@ def validate_url(data, query): raise forms.ValidationError('Please enter a valid {} url'.format(query)) -def change_resume_filename(self, filename): - """ - Changes the filename of an uploaded file to be a unique hash - :param self: the file instance - :param filename: the filename - :return: string that is the new filename - """ - file_ending = filename.split('.')[-1] - from config.settings import SECRET_KEY +class GenericListCreateModel(CreateAPIView, ListAPIView): + permission_classes = (IsAuthenticatedOrReadOnly,) + + def __init__(self): + self.date_of_update = None + super(GenericListCreateModel, self).__init__() + + def get_queryset(self): + self.date_of_update = now_as_utc_epoch() + return parse_date_last_updated(self.request) - from hashlib import sha256 - new_name = sha256(self.user.email + SECRET_KEY).hexdigest() - return new_name[:10] + '.' + file_ending + def list(self, request, *args, **kwargs): + response = super(GenericListCreateModel, self).list(request, *args, **kwargs) + response.data = {'results': response.data, 'date_updated': self.date_of_update} + return response + + # noinspection PyProtectedMember + def create(self, request, *args, **kwargs): + if hasattr(self, 'get_queryset'): + queryset = self.get_queryset() + else: + queryset = getattr(self, 'queryset', None) + + assert queryset is not None, ( + 'Cannot have a GenericListModel with no ' + '`.queryset` or not have defined the `.get_queryset()` method.' + ) + model_class = queryset.model + request_data = request.data.copy() + request_data['approved'] = request.user.has_perm('%(app_label)s.change_%(model_name)s' % + {'app_label': model_class._meta.app_label, + 'model_name': model_class._meta.model_name}) + serializer = self.get_serializer(data=request_data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) + + +class GenericUpdateDestroyModel(RetrieveUpdateDestroyAPIView): + permission_classes = (DjangoModelPermissions,) + lookup_field = 'id' + + +def serialized_user(user): + return {'name': user.get_full_name(), 'email': user.email, + 'school': user.cleaned_school_name(), + 'can_post_announcements': user.has_perm('MHacks.add_announcement'), + 'can_edit_announcements': user.has_perm('MHacks.change_announcement'), + 'can_perform_scan': user.has_perm('MHacks.can_perform_scan')} + + +def mhacks_exception_handler(exc, context): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc, context) + + if not response: + return response + if isinstance(response.data, str): + response.data = {'detail': response} + elif isinstance(response.data, list): + response.data = {'detail': response.data[0]} + elif not response.data.get('detail', None): + if len(response.data) == 0: + response.data = {'detail': 'Unknown error'} + elif isinstance(response.data, list): + response.data = {'detail': response.data[0]} + elif isinstance(response.data, dict): + first_key = response.data.keys()[0] + detail_for_key = response.data[first_key] + if isinstance(detail_for_key, list): + detail_for_key = detail_for_key[0] + if first_key.lower() == 'non_field_errors': + response.data = {'detail': "{}".format(detail_for_key)} + else: + response.data = {'detail': "{}: {}".format(first_key.title(), detail_for_key)} + else: + response.data = {'detail': 'Unknown error'} + return response + + +class UnixEpochDateField(serializers.DateTimeField): + def to_internal_value(self, value): + from datetime import datetime + from pytz import utc + try: + return datetime.utcfromtimestamp(float(value)).replace(tzinfo=utc) + except ValueError: + self.fail('invalid', format='Unix Epoch Timestamp') + + def to_representation(self, value): + import datetime + + if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime): + self.fail('date') + dt = to_utc_epoch(value) + if not dt: + self.fail('invalid', format='Unix Epoch Timestamp') + return dt + + +class DurationInSecondsField(serializers.Field): + error_messages = {'invalid': 'Invalid format expected duration in seconds'} + + def to_internal_value(self, data): + from datetime import timedelta + try: + return timedelta(seconds=int(data)) + except ValueError: + self.fail('invalid') + + def to_representation(self, value): + return value.total_seconds() + + +class NonNullPrimaryKeyField(serializers.PrimaryKeyRelatedField): + def to_representation(self, value): + if not value: + return None + return super(NonNullPrimaryKeyField, self).to_representation(value) + + +def parse_date_last_updated(request): + date_last_updated_raw = request.query_params.get('since', None) + if date_last_updated_raw: + try: + from pytz import utc + from datetime import datetime + return datetime.utcfromtimestamp(float(date_last_updated_raw)).replace(tzinfo=utc) + except ValueError: + print('Value error') + pass + return None + + +def now_as_utc_epoch(): + import pytz + from datetime import datetime + return to_utc_epoch(datetime.now(pytz.utc)) + + +def to_utc_epoch(date_time): + from datetime import datetime + + if isinstance(date_time, datetime): + import pytz + date_time = date_time.astimezone(pytz.utc) + from calendar import timegm + return timegm(date_time.timetuple()) + return None diff --git a/MHacks/v1/__init__.py b/MHacks/v1/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/MHacks/v1/announcements.py b/MHacks/v1/announcements.py deleted file mode 100644 index 4e63c00..0000000 --- a/MHacks/v1/announcements.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.db.models import Q -from django.utils import timezone - -from MHacks.models import Announcement as AnnouncementModel -from MHacks.v1.serializers import AnnouncementSerializer -from MHacks.v1.util import GenericListCreateModel, GenericUpdateDestroyModel - - -class Announcements(GenericListCreateModel): - """ - Announcements are what send push notifications and are useful for pushing updates to MHacks participants. - Anybody who is logged in can make a GET request where as only authorized users can create, update and delete them. - """ - serializer_class = AnnouncementSerializer - query_set = AnnouncementModel.objects.none() - - def get_queryset(self): - date_last_updated = super(Announcements, self).get_queryset() - - if not self.request.user or not self.request.user.has_perm('MHacks.change_announcement'): - query_set = AnnouncementModel.objects.all().filter(approved=True).filter(broadcast_at__lte=timezone.now()) - if date_last_updated: - query_set = query_set.filter(Q(last_updated__gte=date_last_updated) | Q(broadcast_at__gte=date_last_updated)) - else: - query_set = query_set.filter(deleted=False) - else: - query_set = AnnouncementModel.objects.all() - if date_last_updated: - query_set = query_set.filter(last_updated__gte=date_last_updated) - else: - query_set = query_set.filter(deleted=False) - return query_set - - -class Announcement(GenericUpdateDestroyModel): - serializer_class = AnnouncementSerializer - queryset = AnnouncementModel.objects.all() diff --git a/MHacks/v1/events.py b/MHacks/v1/events.py deleted file mode 100644 index 22f19a2..0000000 --- a/MHacks/v1/events.py +++ /dev/null @@ -1,26 +0,0 @@ -from MHacks.models import Event as EventModel -from MHacks.v1.serializers import EventSerializer -from MHacks.v1.util import GenericListCreateModel, GenericUpdateDestroyModel - - -class Events(GenericListCreateModel): - """ - Events are the objects that show up on the calendar view and represent specific planned events at the hackathon. - """ - serializer_class = EventSerializer - query_set = EventModel.objects.none() - - def get_queryset(self): - date_last_updated = super(Events, self).get_queryset() - if date_last_updated: - query_set = EventModel.objects.all().filter(last_updated__gte=date_last_updated) - else: - query_set = EventModel.objects.all().filter(deleted=False) - if not self.request.user or not self.request.user.has_perm('MHacks.change_event'): - return query_set.filter(approved=True) - return query_set - - -class Event(GenericUpdateDestroyModel): - serializer_class = EventSerializer - queryset = EventModel.objects.all() diff --git a/MHacks/v1/floors.py b/MHacks/v1/floors.py deleted file mode 100644 index fd76240..0000000 --- a/MHacks/v1/floors.py +++ /dev/null @@ -1,24 +0,0 @@ -from MHacks.models import Floor as FloorModel -from MHacks.v1.serializers import FloorSerializer -from MHacks.v1.util import GenericListCreateModel, GenericUpdateDestroyModel - - -class Floors(GenericListCreateModel): - """ - Floors are the new map object - """ - serializer_class = FloorSerializer - query_set = FloorModel.objects.none() - - def get_queryset(self): - date_last_updated = super(Floors, self).get_queryset() - if date_last_updated: - query_set = FloorModel.objects.all().filter(last_updated__gte=date_last_updated) - else: - query_set = FloorModel.objects.all().filter(deleted=False) - return query_set - - -class Floor(GenericUpdateDestroyModel): - serializer_class = FloorSerializer - queryset = FloorModel.objects.all().filter(deleted=False) diff --git a/MHacks/v1/locations.py b/MHacks/v1/locations.py deleted file mode 100644 index 0ac3f33..0000000 --- a/MHacks/v1/locations.py +++ /dev/null @@ -1,23 +0,0 @@ -from MHacks.models import Location as LocationModel -from MHacks.v1.serializers import LocationSerializer -from MHacks.v1.util import GenericListCreateModel, GenericUpdateDestroyModel - - -class Locations(GenericListCreateModel): - """ - Locations are specific important locations that can be linked to from other tables. - """ - serializer_class = LocationSerializer - query_set = LocationModel.objects.none() - - def get_queryset(self): - date_last_updated = super(Locations, self).get_queryset() - if date_last_updated: - return LocationModel.objects.all().filter(last_updated__gte=date_last_updated) - else: - return LocationModel.objects.all().filter(deleted=False) - - -class Location(GenericUpdateDestroyModel): - serializer_class = LocationSerializer - queryset = LocationModel.objects.all() diff --git a/MHacks/v1/scan_event.py b/MHacks/v1/scan_event.py deleted file mode 100644 index 7679086..0000000 --- a/MHacks/v1/scan_event.py +++ /dev/null @@ -1,82 +0,0 @@ -from rest_framework.exceptions import ValidationError - -from MHacks.models import ScanEvent as ScanEventModel, Registration, Application -from MHacks.v1.serializers import ScanEventSerializer -from MHacks.v1.util import GenericListCreateModel, GenericUpdateDestroyModel - - -class ScanEvents(GenericListCreateModel): - """ - Announcements are what send push notifications and are useful for pushing updates to MHacks participants. - Anybody who is logged in can make a GET request where as only authorized users can create, update and delete them. - """ - serializer_class = ScanEventSerializer - query_set = ScanEventModel.objects.none() - - def get_queryset(self): - date_last_updated = super(ScanEvents, self).get_queryset() - if date_last_updated: - query_set = ScanEventModel.objects.all().filter(last_updated__gte=date_last_updated) - else: - query_set = ScanEventModel.objects.all().filter(deleted=False) - - return query_set - - -class ScanEvent(GenericUpdateDestroyModel): - serializer_class = ScanEventSerializer - queryset = ScanEventModel.objects.all() - - -# Scan Verifiers -# IMPORTANT NOTE: All scan verifiers must go in this file as a global function, if not -# it will simply not work - -def registration_scan_verify(request, scanned_user): - succeeded = True - application = scanned_user.application_or_none() - registration = scanned_user.registration_or_none() - all_fields = _general_information(scanned_user, application) - if application and application.user_is_minor(): - all_fields.append(_create_field('MINOR', 'Yes', color='FF0000')) - - if not registration or not registration.acceptance: - all_fields.append(error_field('Not registered. Send to registration desk.')) - succeeded = False - return succeeded, all_fields - - -def general_information_scan_verify(request, scanned_user): - return True, _general_information(scanned_user) - - -def swag_scan_verify(request, scanned_user): - succeeded = True - all_fields = [_create_field('NAME', scanned_user.get_full_name())] - registration = scanned_user.registration_or_none() - if not registration: - all_fields.append(error_field('Not registered. Send to registration desk.')) - succeeded = False - else: - all_fields.append(_create_field('T-SHIRT SIZE', registration.t_shirt_size)) - application = scanned_user.application_or_none() - if application and application.mentoring: - all_fields.append(_create_field('MENTOR', 'Yes', color='0000FF')) - - return succeeded, all_fields - - -def _general_information(user, application=None): - return [_create_field('NAME', user.get_full_name()), - _create_field('EMAIL', user.email), - _create_field('SCHOOL', user.cleaned_school_name(application))] - - -def error_field(value): - return _create_field('ERROR', value, color='FF0000') - - -def _create_field(label, value, color='000000'): - return {'label': label, - 'value': value, - 'color': color} diff --git a/MHacks/v1/serializers/__init__.py b/MHacks/v1/serializers/__init__.py deleted file mode 100644 index 0cfc699..0000000 --- a/MHacks/v1/serializers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from MHacks.v1.serializers.serializers import AnnouncementSerializer -from MHacks.v1.serializers.serializers import AuthSerializer -from MHacks.v1.serializers.serializers import EventSerializer -from MHacks.v1.serializers.serializers import LocationSerializer -from MHacks.v1.serializers.serializers import ScanEventSerializer -from MHacks.v1.serializers.serializers import FloorSerializer diff --git a/MHacks/v1/serializers/serializers.py b/MHacks/v1/serializers/serializers.py deleted file mode 100644 index b370924..0000000 --- a/MHacks/v1/serializers/serializers.py +++ /dev/null @@ -1,108 +0,0 @@ -from rest_framework import serializers -from rest_framework.authtoken.serializers import AuthTokenSerializer -from rest_framework.fields import CharField, ChoiceField -from rest_framework.relations import PrimaryKeyRelatedField -from rest_framework.serializers import ModelSerializer - -from MHacks.models import Announcement as AnnouncementModel, \ - Event as EventModel, Location as LocationModel, \ - ScanEvent as ScanEventModel, MHacksUser as MHacksUserModel, Floor as FloorModel -from MHacks.v1.serializers.util import UnixEpochDateField, DurationInSecondsField, NonNullPrimaryKeyField - - -class MHacksModelSerializer(ModelSerializer): - def to_representation(self, instance): - if getattr(instance, 'deleted', False): - # noinspection PyProtectedMember - return {instance._meta.pk.name: str(instance.pk), 'deleted': True} - return super(MHacksModelSerializer, self).to_representation(instance) - - -class AnnouncementSerializer(MHacksModelSerializer): - id = CharField(read_only=True) - broadcast_at = UnixEpochDateField() - - class Meta: - model = AnnouncementModel - fields = ('id', 'title', 'info', 'broadcast_at', 'category', 'approved') - - -class EventSerializer(MHacksModelSerializer): - id = CharField(read_only=True) - start = UnixEpochDateField() - locations = PrimaryKeyRelatedField(many=True, pk_field=CharField(), - queryset=LocationModel.objects.all().filter(deleted=False)) - duration = DurationInSecondsField() - category = ChoiceField(choices=EventModel.CATEGORIES) - - class Meta: - model = EventModel - fields = ('id', 'name', 'info', 'start', 'duration', 'locations', 'category', 'approved') - - -class FloorSerializer(MHacksModelSerializer): - id = CharField(read_only=True) - - class Meta: - model = FloorModel - fields = ('id', 'name', 'image', 'index', 'offset_fraction', 'aspect_ratio', 'description', 'nw_latitude', - 'nw_longitude', 'se_latitude', 'se_longitude') - - -class LocationSerializer(MHacksModelSerializer): - id = CharField(read_only=True) - floor = NonNullPrimaryKeyField(many=False, pk_field=CharField(), - queryset=FloorModel.objects.all().filter(deleted=False), - allow_empty=True) - - class Meta: - model = LocationModel - fields = ('id', 'name', 'floor', 'latitude', 'longitude') - - -class ScanEventSerializer(MHacksModelSerializer): - id = CharField(read_only=True) - expiry_date = UnixEpochDateField() - - class Meta: - model = ScanEventModel - fields = ('id', 'name', 'expiry_date') - - -class MHacksUserSerializer(MHacksModelSerializer): - id = CharField(read_only=True) - - class Meta: - model = MHacksUserModel - fields = ('id', 'first_name', 'last_name', 'email') - - -class AuthSerializer(AuthTokenSerializer): - # Extends auth token serializer to accommodate push notifs - - token = serializers.CharField(required=False) - is_gcm = serializers.BooleanField(required=False) - - def validate(self, attributes): - attributes = super(AuthSerializer, self).validate(attributes) - - # Optionally add the token if it exists - if 'registration_id' in attributes.keys() and 'is_gcm' in attributes.keys(): - token = attributes.get('registration_id') - is_gcm = attributes.get('is_gcm') - preference = attributes.get('name', attributes.get('preference', '63')) - if not isinstance(preference, str): - preference = str(AnnouncementModel.max_category()) - attributes['push_notification'] = { - 'registration_id': token, - 'is_gcm': is_gcm, - 'name': preference - } - - return attributes - - def create(self, validated_data): - pass - - def update(self, instance, validated_data): - pass diff --git a/MHacks/v1/serializers/util.py b/MHacks/v1/serializers/util.py deleted file mode 100644 index d95013d..0000000 --- a/MHacks/v1/serializers/util.py +++ /dev/null @@ -1,72 +0,0 @@ -from rest_framework import serializers - - -class UnixEpochDateField(serializers.DateTimeField): - def to_internal_value(self, value): - from datetime import datetime - from pytz import utc - try: - return datetime.utcfromtimestamp(float(value)).replace(tzinfo=utc) - except ValueError: - self.fail('invalid', format='Unix Epoch Timestamp') - - def to_representation(self, value): - import datetime - - if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime): - self.fail('date') - dt = to_utc_epoch(value) - if not dt: - self.fail('invalid', format='Unix Epoch Timestamp') - return dt - - -class DurationInSecondsField(serializers.Field): - error_messages = {'invalid': 'Invalid format expected duration in seconds'} - - def to_internal_value(self, data): - from datetime import timedelta - try: - return timedelta(seconds=int(data)) - except ValueError: - self.fail('invalid') - - def to_representation(self, value): - return value.total_seconds() - - -class NonNullPrimaryKeyField(serializers.PrimaryKeyRelatedField): - def to_representation(self, value): - if not value: - return None - return super(NonNullPrimaryKeyField, self).to_representation(value) - - -def parse_date_last_updated(request): - date_last_updated_raw = request.query_params.get('since', None) - if date_last_updated_raw: - try: - from pytz import utc - from datetime import datetime - return datetime.utcfromtimestamp(float(date_last_updated_raw)).replace(tzinfo=utc) - except ValueError: - print('Value error') - pass - return None - - -def now_as_utc_epoch(): - import pytz - from datetime import datetime - return to_utc_epoch(datetime.now(pytz.utc)) - - -def to_utc_epoch(date_time): - from datetime import datetime - - if isinstance(date_time, datetime): - import pytz - date_time = date_time.astimezone(pytz.utc) - from calendar import timegm - return timegm(date_time.timetuple()) - return None diff --git a/MHacks/v1/urls.py b/MHacks/v1/urls.py deleted file mode 100644 index dd411f6..0000000 --- a/MHacks/v1/urls.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.conf.urls import url -from rest_framework_docs.views import DRFDocsView - -from MHacks.v1.announcements import Announcements, Announcement -from MHacks.v1.auth import Authentication -from MHacks.v1.events import Events, Event -from MHacks.v1.floors import Floors, Floor -from MHacks.v1.locations import Locations, Location -from MHacks.v1.push_notification_views import APNSTokenView, GCMTokenView -from MHacks.v1.scan_event import ScanEvents, ScanEvent -from MHacks.v1.views import get_countdown, apple_pass_endpoint, update_user_profile, perform_scan, application_breakdown, race_breakdown, gender_breakdown, school_breakdown - -urlpatterns = [ - # Authentication - url(r'^login/$', Authentication.as_view(), name='api-login'), - url(r'^announcements/(?P[0-9A-Za-z_\-]+)$', Announcement.as_view()), - url(r'^announcements/$', Announcements.as_view(), name='announcements'), - url(r'^locations/(?P[0-9A-Za-z_\-]+)$', Location.as_view()), - url(r'^locations/$', Locations.as_view(), name='locations'), - url(r'^events/(?P[0-9A-Za-z_\-]+)$', Event.as_view()), - url(r'^events/$', Events.as_view(), name='events'), - url(r'^floors/(?P[0-9A-Za-z_\-]+)', Floor.as_view()), - url(r'^floors', Floors.as_view(), name='floors'), - url(r'^scan_events/(?P[0-9A-Za-z_\-]+)', ScanEvent.as_view()), - url(r'^scan_events', ScanEvents.as_view(), name='scan_events'), - url(r'^perform_scan/', perform_scan, name='perform_scan'), - url(r'^countdown/$', get_countdown, name='countdown'), - url(r'^profile/$', update_user_profile, name='profile'), - url(r'^push_notifications/apns/$', APNSTokenView.as_view(), name='create_apns_device'), - url(r'^push_notifications/gcm/$', GCMTokenView.as_view(), name='create_gcm_device'), - url(r'^apple_pass/$', apple_pass_endpoint, name='apple_pass_endpoint'), - url(r'^docs/$', DRFDocsView.as_view(template_name='docs.html'), name='docs'), - - # Stats - url(r'^application_breakdown/$', application_breakdown, name='application_breakdown'), - url(r'^race_breakdown/$', race_breakdown, name='race_breakdown'), - url(r'^gender_breakdown/$', gender_breakdown, name='gender_breakdown'), - url(r'^school_breakdown/$', school_breakdown, name='school_breakdown') -] diff --git a/MHacks/v1/util.py b/MHacks/v1/util.py deleted file mode 100644 index 432c396..0000000 --- a/MHacks/v1/util.py +++ /dev/null @@ -1,90 +0,0 @@ -from rest_framework.response import Response -from rest_framework.status import HTTP_201_CREATED -from rest_framework.permissions import DjangoModelPermissions, IsAuthenticatedOrReadOnly -from rest_framework.generics import ListAPIView, CreateAPIView, RetrieveUpdateDestroyAPIView -from rest_framework.views import exception_handler -from MHacks.models import Application - -from MHacks.v1.serializers.util import now_as_utc_epoch, parse_date_last_updated - - -class GenericListCreateModel(CreateAPIView, ListAPIView): - permission_classes = (IsAuthenticatedOrReadOnly,) - - def __init__(self): - self.date_of_update = None - super(GenericListCreateModel, self).__init__() - - def get_queryset(self): - self.date_of_update = now_as_utc_epoch() - return parse_date_last_updated(self.request) - - def list(self, request, *args, **kwargs): - response = super(GenericListCreateModel, self).list(request, *args, **kwargs) - response.data = {'results': response.data, 'date_updated': self.date_of_update} - return response - - # noinspection PyProtectedMember - def create(self, request, *args, **kwargs): - if hasattr(self, 'get_queryset'): - queryset = self.get_queryset() - else: - queryset = getattr(self, 'queryset', None) - - assert queryset is not None, ( - 'Cannot have a GenericListModel with no ' - '`.queryset` or not have defined the `.get_queryset()` method.' - ) - model_class = queryset.model - request_data = request.data.copy() - request_data['approved'] = request.user.has_perm('%(app_label)s.change_%(model_name)s' % - {'app_label': model_class._meta.app_label, - 'model_name': model_class._meta.model_name}) - serializer = self.get_serializer(data=request_data) - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) - - -class GenericUpdateDestroyModel(RetrieveUpdateDestroyAPIView): - permission_classes = (DjangoModelPermissions,) - lookup_field = 'id' - - -def serialized_user(user): - return {'name': user.get_full_name(), 'email': user.email, - 'school': user.cleaned_school_name(), - 'can_post_announcements': user.has_perm('MHacks.add_announcement'), - 'can_edit_announcements': user.has_perm('MHacks.change_announcement'), - 'can_perform_scan': user.has_perm('MHacks.can_perform_scan')} - - -def mhacks_exception_handler(exc, context): - # Call REST framework's default exception handler first, - # to get the standard error response. - response = exception_handler(exc, context) - - if not response: - return response - if isinstance(response.data, str): - response.data = {'detail': response} - elif isinstance(response.data, list): - response.data = {'detail': response.data[0]} - elif not response.data.get('detail', None): - if len(response.data) == 0: - response.data = {'detail': 'Unknown error'} - elif isinstance(response.data, list): - response.data = {'detail': response.data[0]} - elif isinstance(response.data, dict): - first_key = response.data.keys()[0] - detail_for_key = response.data[first_key] - if isinstance(detail_for_key, list): - detail_for_key = detail_for_key[0] - if first_key.lower() == 'non_field_errors': - response.data = {'detail': "{}".format(detail_for_key)} - else: - response.data = {'detail': "{}: {}".format(first_key.title(), detail_for_key)} - else: - response.data = {'detail': 'Unknown error'} - return response diff --git a/MHacks/v1/views.py b/MHacks/v1/views.py deleted file mode 100644 index a0bf885..0000000 --- a/MHacks/v1/views.py +++ /dev/null @@ -1,196 +0,0 @@ -import base64 -from datetime import datetime - -from django.contrib.auth import get_user_model -from django.db.models import Sum, Count -from django.http import HttpResponseForbidden -from pytz import utc -from rest_framework.decorators import api_view, permission_classes -from rest_framework.exceptions import ValidationError -from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated, BasePermission -from rest_framework.response import Response - -from MHacks.models import ScanEvent, ScanEventUser, Application -from MHacks.pass_creator import create_apple_pass -from MHacks.v1.serializers.util import now_as_utc_epoch, parse_date_last_updated, to_utc_epoch -from MHacks.v1.util import serialized_user - - -@api_view(http_method_names=['GET']) -@permission_classes((IsAuthenticatedOrReadOnly,)) -def get_countdown(request): - """ - Gets the countdown representation for the hackathon - """ - # Update the date_updated to your current time if you modify the return value of the countdown - date_updated = datetime(year=2017, month=3, day=19, hour=0, minute=0, second=0, microsecond=0, tzinfo=utc) - - client_updated = parse_date_last_updated(request) - if client_updated and client_updated >= date_updated: - return Response(data={'date_updated': now_as_utc_epoch()}) - - start_time = datetime(year=2017, month=3, day=25, hour=4, minute=0, second=0, microsecond=0, - tzinfo=utc) - return Response(data={'start_time': to_utc_epoch(start_time), - 'countdown_duration': 129600, # 36 hours - 'hacks_submitted': 118800, # 33 hours - 'date_updated': now_as_utc_epoch()}) - - -@api_view(http_method_names=['GET']) -@permission_classes((IsAuthenticatedOrReadOnly,)) -def apple_site_association(request): - return Response(data={"webcredentials": {"apps": ["478C74MJ7T.com.MPowered.MHacks"]}}) - - -@api_view(http_method_names=['GET']) -@permission_classes((IsAuthenticated,)) -def apple_pass_endpoint(request): - return Response(data={"apple_pass": base64.encodestring(create_apple_pass(request.user).getvalue())}) - - -@api_view(http_method_names=['GET']) -@permission_classes((IsAuthenticated,)) -def update_user_profile(request): - return Response(data=serialized_user(request.user)) - - -class CanPerformScan(BasePermission): - def has_permission(self, request, view): - return request.user and request.user.is_authenticated and request.user.has_perm('MHacks.can_perform_scan') - - -@api_view(http_method_names=['POST', 'GET']) -@permission_classes((CanPerformScan,)) -def perform_scan(request): - from scan_event import error_field - if request.method == 'POST': - information = request.POST - else: - information = request.GET - - scan_event_id = information.get('scan_event', None) - user_id = information.get('user_id', None) - if not scan_event_id or not user_id: - raise ValidationError('Invalid fields provided') - - try: - scan_event = ScanEvent.objects.get(pk=scan_event_id) - user = get_user_model().objects.get(email=user_id) - except (ScanEvent.DoesNotExist, get_user_model().DoesNotExist): - raise ValidationError('Invalid scan event or user') - - if scan_event.expiry_date: - if scan_event.deleted or to_utc_epoch(scan_event.expiry_date) < now_as_utc_epoch(): - raise ValidationError('Scan event is no longer valid') - - successful_scan = True - error = None - data = [] - scan_event_user_join = None - if scan_event.number_of_allowable_scans: - try: - scan_event_user_join = ScanEventUser.objects.get(user=user, scan_event=scan_event) - number_of_scans = scan_event_user_join.count - except (ScanEventUser.DoesNotExist, get_user_model().DoesNotExist): - number_of_scans = 0 - - if number_of_scans >= scan_event.number_of_allowable_scans: - successful_scan = False - error = error_field('Can\'t scan again') - - success = True - if scan_event.custom_verification: - import MHacks.v1.scan_event as scan_event_verifiers - try: - success, data = getattr(scan_event_verifiers, scan_event.custom_verification)(request, user) - except AttributeError: - pass # This shouldn't happen normally but we defensively protect against it - successful_scan = successful_scan and success - if error: - data.append(error) - scan_result = {'scanned': successful_scan, 'data': data} - - # Only if its a POST request do we actually "do" the scan - # GET requests are peeks i.e. they don't modify the database at all - # If there is no number_of_allowable_scans we don't do anything on a POST either (unlimited) - if successful_scan and scan_event.number_of_allowable_scans and request.method == 'POST': - if scan_event_user_join: - scan_event_user_join.count += 1 - else: - scan_event_user_join = ScanEventUser(user=user, scan_event=scan_event, count=1) - scan_event_user_join.save() - - return Response(data=scan_result) - - -@api_view(http_method_names=['GET']) -def application_breakdown(request): - if not request.user or not request.user.groups.filter(name='stats_team').exists(): - return HttpResponseForbidden() - - accepted = Application.objects.filter(decision='Accept') - waitlisted = Application.objects.filter(decision='Waitlist') - declined = Application.objects.filter(decision='Decline') - total_reimbursement = Application.objects.aggregate(Sum('reimbursement'))['reimbursement__sum'] - total_applications = accepted.count() + waitlisted.count() + declined.count() - avg_reimbursement = total_reimbursement / accepted.count() - - return Response(data={'accepted': accepted.count(), - 'waitlisted': waitlisted.count(), - 'declined': declined.count(), - 'total_applications': total_applications, - 'total_reimbursement': total_reimbursement, - 'avg_reimbursement': avg_reimbursement}) - - -@api_view(http_method_names=['GET']) -def race_breakdown(request): - from MHacks.application_lists import DEMOGRAPHIC_INFO - - if not request.user or not request.user.groups.filter(name='stats_team').exists(): - return HttpResponseForbidden() - - race_stats = list() - for value, label in DEMOGRAPHIC_INFO: - apps = Application.objects.filter(race=value) - race_stats.append({ - 'label': label, - 'count': apps.count() - }) - - return Response(data=race_stats) - - -@api_view(http_method_names=['GET']) -def gender_breakdown(request): - from MHacks.application_lists import GENDER - - if not request.user or not request.user.groups.filter(name='stats_team').exists(): - return HttpResponseForbidden() - - gender_stats = list() - for value, label in GENDER: - apps = Application.objects.filter(gender=value) - gender_stats.append({ - 'label': label, - 'count': apps.count() - }) - - return Response(data=gender_stats) - - -@api_view(http_method_names=['GET']) -def school_breakdown(request): - if not request.user or not request.user.groups.filter(name='stats_team').exists(): - return HttpResponseForbidden() - - school_stats = list() - results = Application.objects.filter(decision='Accept').values('school').annotate(count=Count('school')) - for school in results: - school_stats.append({ - 'label': school['school'], - 'count': school['count'] - }) - - return Response(data=school_stats) diff --git a/MHacks/v1_urls.py b/MHacks/v1_urls.py new file mode 100644 index 0000000..a930389 --- /dev/null +++ b/MHacks/v1_urls.py @@ -0,0 +1,34 @@ +from django.conf.urls import url +from rest_framework_docs.views import DRFDocsView + +from MHacks.events import EventListAPIView, EventAPIView +from MHacks.locations import LocationListAPIView, LocationAPIView +from MHacks.scan_events import ScanEventListAPIView, ScanEventAPIView +from views import get_countdown +from announcements import AnnouncementListAPIView, AnnouncementAPIView +from authentication import AuthenticationAPIView +from floors import FloorListAPIView, FloorAPIView +from push_notifications import APNSTokenView, GCMTokenView +from scan_events import perform_scan +from users import update_user_profile + +urlpatterns = [ + # Authentication + url(r'^login/$', AuthenticationAPIView.as_view(), name='api-login'), + url(r'^announcements/(?P[0-9A-Za-z_\-]+)$', AnnouncementAPIView.as_view()), + url(r'^announcements/$', AnnouncementListAPIView.as_view(), name='announcements'), + url(r'^locations/(?P[0-9A-Za-z_\-]+)$', LocationAPIView.as_view()), + url(r'^locations/$', LocationListAPIView.as_view(), name='locations'), + url(r'^events/(?P[0-9A-Za-z_\-]+)$', EventAPIView.as_view()), + url(r'^events/$', EventListAPIView.as_view(), name='events'), + url(r'^floors/(?P[0-9A-Za-z_\-]+)', FloorAPIView.as_view()), + url(r'^floors', FloorListAPIView.as_view(), name='floors'), + url(r'^scan_events/(?P[0-9A-Za-z_\-]+)', ScanEventAPIView.as_view()), + url(r'^scan_events', ScanEventListAPIView.as_view(), name='scan_events'), + url(r'^perform_scan/', perform_scan, name='perform_scan'), + url(r'^countdown/$', get_countdown, name='countdown'), + url(r'^profile/$', update_user_profile, name='profile'), + url(r'^push_notifications/apns/$', APNSTokenView.as_view(), name='create_apns_device'), + url(r'^push_notifications/gcm/$', GCMTokenView.as_view(), name='create_gcm_device'), + url(r'^docs/$', DRFDocsView.as_view(template_name='docs.html'), name='docs'), +] diff --git a/MHacks/views.py b/MHacks/views.py index 48e2503..80a0616 100644 --- a/MHacks/views.py +++ b/MHacks/views.py @@ -1 +1,229 @@ # Nothing here, just to make django happy! +from datetime import datetime + +import mailchimp +from django.contrib.auth import login as auth_login, logout as auth_logout, get_user_model +from django.contrib.auth.decorators import login_required +from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse +from django.http import HttpResponseBadRequest +from django.http import (HttpResponseNotAllowed, + HttpResponseForbidden) +from django.shortcuts import render, redirect +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode, is_safe_url +from django.views.decorators.csrf import csrf_exempt +from pytz import utc +from rest_framework.authtoken.models import Token +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.response import Response + +from MHacks.decorator import anonymous_required +from MHacks.forms import RegisterForm, LoginForm +from MHacks.utils import send_verification_email, send_password_reset_email, validate_signed_token +from config.settings import LOGIN_REDIRECT_URL +from development_settings import MAILCHIMP_API_KEY +from utils import parse_date_last_updated, now_as_utc_epoch, to_utc_epoch + + +def thanks_registering(request): + return render(request, 'thanks_registering.html') + + +@anonymous_required +def login(request): + """ + A lot of this code is identical to the default login code but we have a few hooks (like username query) + and modifications so we implement it ourselves + """ + from django.contrib.auth.views import REDIRECT_FIELD_NAME + redirect_to = request.POST.get(REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME, '')) + if request.method == "POST": + form = LoginForm(request, data=request.POST) + if form.is_valid(): + # Ensure the user-originating redirection url is safe. + if not is_safe_url(url=redirect_to, host=request.get_host()): + redirect_to = reverse(LOGIN_REDIRECT_URL) + + # Okay, security check complete. Log the user in. + auth_login(request, form.get_user()) + + return redirect(redirect_to) + elif request.method == "GET": + form = LoginForm(request, initial=request.GET) + else: + return HttpResponseNotAllowed(permitted_methods=['GET', 'POST']) + context = { + 'form': form, + REDIRECT_FIELD_NAME: redirect_to, + } + return render(request, 'login.html', context) + + +def logout(request): + """ + We are nice about logout requests where if we are not logged in we fail silently. + However, we only accept POST requests for logout, not doing so is a security vulnerability, i.e. CSRF + attacks will be trivial (although not detrimental for security it can be bothersome to our users if malicious + users decide to use this attack) + """ + if request.method != 'POST': + return HttpResponseNotAllowed(permitted_methods=['POST']) + auth_logout(request) + return redirect(reverse('mhacks-home')) + + +@anonymous_required +def register(request): + user_pk = None + if request.method == 'POST': + form = RegisterForm(request.POST) + if form.is_valid(): + user = get_user_model().objects.create_user( + email=form.cleaned_data['email'], + password=form.cleaned_data['password1'], + first_name=form.cleaned_data['first_name'], + last_name=form.cleaned_data['last_name'], + request=request + ) + user.save() + user_pk = urlsafe_base64_encode(force_bytes(user.pk)) + form = None + return redirect(reverse('mhacks-thanks-registering')) + elif request.method == 'GET': + form = RegisterForm() + else: + return HttpResponseNotAllowed(permitted_methods=['POST', 'GET']) + return render(request, 'register.html', {'form': form, 'user_pk': user_pk}) + + +@anonymous_required +def request_verification_email(request, user_pk): + user = validate_signed_token(user_pk, None, require_token=False) + if user is not None: + send_verification_email(user, request) + return redirect(reverse('mhacks-login') + '?username=' + user.email) + + +# CSRF exempt because we need to allow a POST from mobile clients and marking this exempt cannot cause +# any security vulnerabilities since it is @anonymous_required +@csrf_exempt +@anonymous_required +def reset_password(request): + reset_type = 'reset_request' + if request.method == 'POST': + form = PasswordResetForm(request.POST) + if form.is_valid(): + try: + user = get_user_model().objects.get(email=form.cleaned_data["email"]) + except ObjectDoesNotExist: + form.errors['email'] = ["No user with that email exists"] + return render(request, 'password_reset.html', context={'form': form, 'type': reset_type}) + if user: + send_password_reset_email(user, request) + return redirect(reverse('mhacks-password_reset_sent')) + elif request.method == 'GET': + form = PasswordResetForm() + else: + return HttpResponseNotAllowed(permitted_methods=['GET', 'POST']) + if form: + form.fields['email'].longest = True + return render(request, 'password_reset.html', context={'form': form, 'type': reset_type}) + + +def password_reset_sent(request): + return render(request, 'password_reset_sent.html') + + +@anonymous_required +def validate_email(request, uid, token): + user = validate_signed_token(uid, token) + if user is None: + return HttpResponseForbidden() + user.email_verified = True + user.save() + return redirect(reverse('mhacks-login') + '?username=' + user.email) + + +@anonymous_required +def update_password(request, uid, token): + user = validate_signed_token(uid, token) + if not user: + return HttpResponseForbidden() # Just straight up forbid this request, looking fishy already! + if request.method == 'POST': + form = SetPasswordForm(user, data=request.POST) + if form.is_valid(): + form.save() + Token.objects.filter(user_id__exact=user.pk).delete() + return redirect(reverse('mhacks-login') + '?username=' + user.email) + elif request.method == 'GET': + form = SetPasswordForm(user) + else: + return HttpResponseNotAllowed(permitted_methods=['GET', 'POST']) + form.fields['new_password2'].label = 'Confirm New Password' + form.fields['new_password2'].longest = True + return render(request, 'password_reset.html', {'form': form, 'type': 'reset', 'uid': uid, 'token': token}) + + +@login_required +def dashboard(request): + if request.method == 'GET': + return render(request, 'dashboard.html') + return HttpResponseNotAllowed(permitted_methods=['GET']) + + +@api_view(http_method_names=['GET']) +@permission_classes((IsAuthenticatedOrReadOnly,)) +def apple_site_association(request): + return Response(data={"webcredentials": {"apps": ["478C74MJ7T.com.MPowered.MHacks"]}}) + + +MAILCHIMP_API = mailchimp.Mailchimp(MAILCHIMP_API_KEY) + + +def blackout(request): + if request.method == 'POST': + if 'email' not in request.POST: + return HttpResponseBadRequest() + + email = request.POST.get("email") + list_id = "d9245d6d34" + # noinspection PyBroadException + try: + MAILCHIMP_API.lists.subscribe(list_id, {'email': email}, double_optin=False) + except mailchimp.ListAlreadySubscribedError: + return render(request, 'blackout.html', {'error': 'Looks like you\'re already subscribed!'}) + except Exception: + return render(request, 'blackout.html', { + 'error': 'Looks like there\'s been an error registering you. Try again or email us at hackathon@umich.edu'}) + return render(request, 'blackout.html', {'success': True}) + elif request.method == 'GET': + return render(request, 'blackout.html', {}) + else: + return HttpResponseNotAllowed(permitted_methods=['GET', 'POST']) + + +def index(request): + return render(request, 'index.html') + + +@api_view(http_method_names=['GET']) +@permission_classes((IsAuthenticatedOrReadOnly,)) +def get_countdown(request): + """ + Gets the countdown representation for the hackathon + """ + # Update the date_updated to your current time if you modify the return value of the countdown + date_updated = datetime(year=2017, month=3, day=19, hour=0, minute=0, second=0, tzinfo=utc) + + client_updated = parse_date_last_updated(request) + if client_updated and client_updated >= date_updated: + return Response(data={'date_updated': now_as_utc_epoch()}) + + start_time = datetime(year=2017, month=3, day=25, hour=4, minute=0, second=0, tzinfo=utc) + return Response(data={'start_time': to_utc_epoch(start_time), + 'countdown_duration': 129600, # 36 hours + 'hacks_submitted': 118800, # 33 hours + 'date_updated': now_as_utc_epoch()}) diff --git a/config/development_settings.py b/config/development_settings.py index 84185f1..9949552 100644 --- a/config/development_settings.py +++ b/config/development_settings.py @@ -55,9 +55,3 @@ # ^ This is for the blackout interest link APPLE_WALLET_PASSPHRASE = '' - -AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID', '') -AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY', '') -AWS_ACL_POLICY = os.getenv('AWS_ACL_POLICY', 'private') -BOTO_S3_BUCKET = os.getenv('BOTO_S3_BUCKET', 'mhacks-9-resumes') -SECRET_KEY = os.getenv('SECRET_KEY', 'mhacks') diff --git a/config/settings.py b/config/settings.py index a3f936b..14964bd 100644 --- a/config/settings.py +++ b/config/settings.py @@ -117,7 +117,7 @@ 'DEFAULT_RENDERER_CLASSES': [ 'rest_framework.renderers.JSONRenderer', ], - 'EXCEPTION_HANDLER': 'MHacks.v1.util.mhacks_exception_handler', + 'EXCEPTION_HANDLER': 'MHacks.utils.mhacks_exception_handler', 'URL_FORMAT_OVERRIDE': None } @@ -143,9 +143,7 @@ APPEND_SLASH = True -MEDIA_ROOT = 'resumes/' - # SQL explorer settings -EXPLORER_PERMISSION_VIEW = lambda u: u.is_superuser +EXPLORER_PERMISSION_VIEW = lambda user: user.is_superuser # EXPLORER_CONNECTION_NAME = -EXPLORER_SQL_WHITELIST = ('UPDATE') +EXPLORER_SQL_WHITELIST = 'UPDATE' diff --git a/config/urls.py b/config/urls.py index b68a18b..6609088 100644 --- a/config/urls.py +++ b/config/urls.py @@ -16,12 +16,11 @@ """ from django.conf.urls import url, include from django.contrib.admin import site -from MHacks.frontend.urls import urlpatterns as frontend_urls -from MHacks.v1.urls import urlpatterns as api_urls -from MHacks.v1.views import apple_site_association + +from MHacks.urls import urlpatterns as frontend_urls +from MHacks.v1_urls import urlpatterns as api_urls urlpatterns = [ - url(r'^apple-app-site-association', apple_site_association), url(r'^admin/', include(site.urls)), url(r'^v1/', include(api_urls)), url(r'', include(frontend_urls)), diff --git a/requirements.txt b/requirements.txt index 677f60d..82cfb0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ requests==2.9.1 mandrill==1.0.57 django_extensions==1.6.7 mailchimp==2.0.9 -pytz==2016.4 +pytz==2013.7 psycopg2==2.6.1 djangorestframework==3.3.3 drfdocs==0.0.9 diff --git a/static/javascript/announcements.js b/static/javascript/announcements.js index 68830c2..063fcb3 100644 --- a/static/javascript/announcements.js +++ b/static/javascript/announcements.js @@ -68,15 +68,19 @@ refresh.click(function(){ }); function mapCategoryIndex(category) { + //noinspection JSBitwiseOperatorUsage if (category & 1) { return 0; } + //noinspection JSBitwiseOperatorUsage if (category & 2) { return 1; } + //noinspection JSBitwiseOperatorUsage if (category & 4) { return 2; } + //noinspection JSBitwiseOperatorUsage if (category & 8) { return 3; } @@ -84,18 +88,23 @@ function mapCategoryIndex(category) { } function formatAnnouncementCategoryIdentifier(category) { + //noinspection JSBitwiseOperatorUsage if (category & 1) { return "Emergency"; } + //noinspection JSBitwiseOperatorUsage if (category & 2) { return "Logistics"; } + //noinspection JSBitwiseOperatorUsage if (category & 4) { return "Food"; } + //noinspection JSBitwiseOperatorUsage if (category & 8) { return "Event"; } + //noinspection JSBitwiseOperatorUsage if (category & 16) { return "Sponsor"; }