diff --git a/back/Pipfile b/back/Pipfile index b2fa8cc..c996bfc 100644 --- a/back/Pipfile +++ b/back/Pipfile @@ -9,7 +9,8 @@ verify_ssl = true django = "==2.1.5" django-cors-headers = "==2.4.0" djangorestframework = "==3.8.2" - +django-rest-auth = "==0.9.3" +django-allauth = "==0.37.1" gunicorn = "==19.6.0" [requires] diff --git a/back/Pipfile.lock b/back/Pipfile.lock index 4527d5b..a6ed173 100644 --- a/back/Pipfile.lock +++ b/back/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b5222b4256c8f09a9b1b1d380285fa65c443f84d28dc03450684fca84b38a26b" + "sha256": "b1fc6b06ec8daa4efd9573865bc6c1732ae9354309e036bfe3ce0ab76b1a3bcd" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,27 @@ ] }, "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "defusedxml": { + "hashes": [ + "sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4", + "sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20" + ], + "version": "==0.5.0" + }, "django": { "hashes": [ "sha256:a32c22af23634e1d11425574dce756098e015a165be02e4690179889b207c7a8", @@ -24,6 +45,13 @@ "index": "pypi", "version": "==2.1.5" }, + "django-allauth": { + "hashes": [ + "sha256:02175aa1c2ddfd935a54011d1196d70c976647fc46f603f8b8758fc395b9d277" + ], + "index": "pypi", + "version": "==0.37.1" + }, "django-cors-headers": { "hashes": [ "sha256:5545009c9b233ea7e70da7dbab7cb1c12afa01279895086f98ec243d7eab46fa", @@ -32,6 +60,13 @@ "index": "pypi", "version": "==2.4.0" }, + "django-rest-auth": { + "hashes": [ + "sha256:ad155a0ed1061b32e3e46c9b25686e397644fd6acfd35d5c03bc6b9d2fc6c82a" + ], + "index": "pypi", + "version": "==0.9.3" + }, "djangorestframework": { "hashes": [ "sha256:b6714c3e4b0f8d524f193c91ecf5f5450092c2145439ac2769711f7eba89a9d9", @@ -48,12 +83,61 @@ "index": "pypi", "version": "==19.6.0" }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "oauthlib": { + "hashes": [ + "sha256:0ce32c5d989a1827e3f1148f98b9085ed2370fc939bf524c9c851d8714797298", + "sha256:3e1e14f6cde7e5475128d30e97edc3bfb4dc857cb884d8714ec161fdbb3b358e" + ], + "version": "==3.0.1" + }, + "python3-openid": { + "hashes": [ + "sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa", + "sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502" + ], + "version": "==3.1.0" + }, "pytz": { "hashes": [ "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" ], "version": "==2018.9" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "version": "==2.21.0" + }, + "requests-oauthlib": { + "hashes": [ + "sha256:bd6533330e8748e94bf0b214775fed487d309b8b8fe823dc45641ebcd9a32f57", + "sha256:d3ed0c8f2e3bbc6b344fa63d6f933745ab394469da38db16bdddb461c7e25140" + ], + "version": "==1.2.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" } }, "develop": {} diff --git a/back/backend/__init__.py b/back/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/back/backend/migrations/0005_field_field_name.py b/back/backend/migrations/0005_field_field_name.py new file mode 100644 index 0000000..caf25f5 --- /dev/null +++ b/back/backend/migrations/0005_field_field_name.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2019-02-07 22:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('backend', '0004_auto_20190131_1645'), + ] + + operations = [ + migrations.AddField( + model_name='field', + name='field_name', + field=models.CharField(default='field', max_length=512), + ), + ] diff --git a/back/backend/models.py b/back/backend/models.py index bb26ed3..032ed0c 100644 --- a/back/backend/models.py +++ b/back/backend/models.py @@ -1,6 +1,7 @@ from django.db import models from django.conf import settings import datetime +import ntpath class Report(models.Model): user_id = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) @@ -26,17 +27,20 @@ class Section(models.Model): class Field(models.Model): section_id = models.ForeignKey(Section, on_delete=models.CASCADE) + field_name = models.CharField(max_length=512, default="field") label = models.CharField(max_length=512) number = models.IntegerField() type = models.CharField(max_length=128) completed = models.BooleanField(default=False) data_bool = models.BooleanField(default=False) - data_decimal = models.DecimalField(max_digits=9,decimal_places=2, null=True, blank=True) + data_decimal = models.DecimalField(max_digits=9, decimal_places=2, null=True, blank=True) data_date = models.DateField(default=datetime.date.today) data_file = models.FileField(upload_to='uploads/%Y/%m/%d/', max_length=512, null=True, blank=True) data_string = models.TextField(default='', blank=True) data_integer = models.IntegerField(default=0, blank=True) + # function that prints the string representation + # on the api? def __str__(self): if self.type == "boolean": if self.data_bool: @@ -53,3 +57,31 @@ class Field(models.Model): return "{}".format(self.data_string) elif self.type == "integer": return "{}".format(self.data_integer) + + + # function that gets corresponding + # data type + def get_datatype(self): + if self.type == "boolean": + if self.data_bool: + return True + else: + return False + elif self.type == "decimal": + return self.data_decimal + elif self.type == "date": + return "{}".format(self.data_date) + elif self.type == "file": + file_name = self.path_leaf(str(self.data_file)) + return "{}".format(file_name) + elif self.type == "string": + return "{}".format(self.data_string) + elif self.type == "integer": + return self.data_integer + + # function that accommodates if + # path has slash at end + def path_leaf(self, path): + head, tail = ntpath.split(path) + return tail or ntpath.basename(head) + diff --git a/back/backend/policy.py b/back/backend/policy.py index 95e401b..e325284 100644 --- a/back/backend/policy.py +++ b/back/backend/policy.py @@ -1,10 +1,37 @@ -# simple_policy.py from datetime import date -from policy import Policy, Section -# - For the rules, should one refer to fields by 'section.fields.x' -# or by the section name eg. 'general_section.fields.x'? +#### Classes for policy, sections. +class Policy(): + + def __init__(self): + self.sections = [] + + def add_section(self, section): + self.sections.append(section) + +class Section(): + + def __init__(self, title="Section", html_description="", required=False, + auto_submit=False, fields={}): + self.title = title + self.html_description = html_description + self.required = required + self.auto_submit = auto_submit + self.fields = fields + self.rules = [] + + def add_rule(self, title="Rule", rule=None, rule_break_text=""): + rule = { + "title": title, + "rule": rule, + "rule_break_text": rule_break_text, + } + self.rules.append(rule) + +#### Policy configuration begin here + +pol = Policy() #### General #### Section 0 @@ -12,7 +39,7 @@ general_section = Section( title="General Info", html_description="", fields={ - "destination": {"label": "Destination City", "type": "string"} + "destination": {"label": "Destination City", "type": "string"}, } ) @@ -22,7 +49,7 @@ general_section.add_rule( rule_break_text="What did the cowboy say about Tim, his wild horse?" ) -Policy.add_section(general_section) +pol.add_section(general_section) #### Flight #### Section 1 @@ -34,6 +61,7 @@ flight_section = Section( "departure_date": {"label": "Departure date", "type": "date"}, "return_date": {"label": "Return date", "type": "date"}, "fare": {"label": "Fare", "type": "decimal"}, + "layovers": {"label": "Transit wait", "type": "integer"}, } ) @@ -43,13 +71,14 @@ flight_section.add_rule( rule_break_text="Fares cannot be more than $500" ) -Policy.add_section(flight_section) +pol.add_section(flight_section) #### Lodging #### Section 2 lodging_section = Section( title="Hotel Info", - html_description="

Enter hotel info here.\nPer diem rates can be found at

", + html_description="

Enter hotel info here.\nPer diem rates can be found at " + "

", fields={ "check-in_date": {"label": "Check-in date", "type": "date"}, "check-out_date": {"label": "Check-out date", "type": "date"}, @@ -64,13 +93,13 @@ def nightly_rate_check(report, section): duration = checkout_date - checkin_date return section.fields.cost <= duration * section.fields.rate -section.add_rule( +lodging_section.add_rule( title="", rule=nightly_rate_check, rule_break_text="The average nightly rate cannot be more than the USGSA rate." ) -Policy.add_section(lodging_section) +pol.add_section(lodging_section) #### Local Transportation #### Section 3 @@ -89,7 +118,7 @@ transport_section.add_rule( rule_break_text="Local transportation costs must be less than $10 per day, on average." ) -Policy.add_section(transport_section) +pol.add_section(transport_section) #### Per Diem #### Section 4 @@ -109,24 +138,4 @@ per_diem_section.add_rule( rule_break_text="The average cost per day for per diem expenses cannot be more than the rate specified by the USGSA." ) -Policy.add_section(per_diem_section) - -''' -Section( - title="", - html_description="

", - fields={ - "": {"label": "", "type": ""} - } -) - -section.add_rule( - title="", - rule=lambda report, section: boolean_statement, - rule_break_text="" -) - -#// or, for a rule which doesn’t apply to a specific section... -#// -#// add_general_rule(...) -''' +pol.add_section(per_diem_section) diff --git a/back/backend/serializers.py b/back/backend/serializers.py deleted file mode 100644 index 1b162f4..0000000 --- a/back/backend/serializers.py +++ /dev/null @@ -1,66 +0,0 @@ -# Rupika Dikkala -# January 23, 2019 -# File contains serializers needed -# to set up API end points - -from rest_framework import serializers -from . import models - -# serializer for reports -class ReportSerializer(serializers.ModelSerializer): - # user id is foreign key - user_id = serializers.PrimaryKeyRelatedField(many=False, read_only=True) - - class Meta: - fields = ( - 'user_id', - 'title', - 'date_created', - # 'data_submitted', - 'submitted', - ) - model = models.Report - - -# section serializer -class SectionSerializer(serializers.ModelSerializer): - # report id foriegn key - report_id = serializers.PrimaryKeyRelatedField(many=True, read_only=True) - - - class Meta: - fields = ( - 'report_id', - 'completed', - 'title', - 'html_description', - 'number', - ) - model = models.Section - - -class FieldSerializer(serializers.ModelSerializer): - # section_id is foriegn key - section_id = serializers.PrimaryKeyRelatedField(many=True, read_only=True) - - class Meta: - fields = ( - 'section_id', - 'label', - 'number', - 'type', - 'completed', - ) - model = models.Field - - -class DataSerializer(serializers.ModelSerializer): - field_id = serializers.PrimaryKeyRelatedField(many=False, read_only=True) - - - - - - - - diff --git a/back/backend/test.py b/back/backend/test.py new file mode 100644 index 0000000..6ecdccb --- /dev/null +++ b/back/backend/test.py @@ -0,0 +1,3 @@ +from policy import pol + +print(pol) diff --git a/back/backend/urls.py b/back/backend/urls.py index 8e0cada..83f7279 100644 --- a/back/backend/urls.py +++ b/back/backend/urls.py @@ -5,16 +5,10 @@ from rest_framework.urlpatterns import format_suffix_patterns from . import views urlpatterns = [ - #path('', views.List.as_view()), - #path('/', views.Detail.as_view()), - path('report', views.report), path('reports', views.reports), path('report/', views.report_detail), path('report//section/', views.section), - path('account', views.account), - path('account/login', views.account_login), - path('account/logout', views.account_logout), ] -urlpatterns = format_suffix_patterns(urlpatterns) \ No newline at end of file +urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/back/backend/views.py b/back/backend/views.py index 1f99059..98cb155 100644 --- a/back/backend/views.py +++ b/back/backend/views.py @@ -1,194 +1,145 @@ -from rest_framework import generics -from rest_framework import status from rest_framework.decorators import api_view -from django.shortcuts import render from django.http import JsonResponse from .models import * -from .serializers import * +from .policy import pol +import ntpath -# Sample view using generics -class List(generics.ListCreateAPIView): - queryset = Report.objects.all() - serializer_class = ReportSerializer +# function that prints all the reports +def get_reports(report_pk): + queryset = Report.objects.filter(id=report_pk) + for i in queryset: + data = { + "report_pk": report_pk, + "title": i.title, + "date_created": i.date_created, + "submitted": i.submitted, + "date_submitted": i.date_submitted, + } + # append the sections for each report + data.update(get_sections(i.id)) + + # return JsonResponse(data) + return data + +# function that gets all the sections +# takes report_id param +def get_sections(r_id): + # create a dict of arrays for section + section_set = {"sections": []} + queryset = Section.objects.filter(report_id=r_id) + for i in queryset: + data = { + "id": i.id, + "completed": i.completed, + "title": i.title, + "html_description": i.html_description, + } + # append the fields for corresponding section + data.update(get_fields(i.id)) + # append section to the array + section_set["sections"].append(data.copy()) + + return section_set + +# function that gets all the fields +# takes section_id param +def get_fields(s_id): + # create dict of arrays for fields + field_set = {"fields": []} + queryset = Field.objects.filter(section_id=s_id).order_by('number') + + for i in queryset: + # function that gets the corresponding datatype + value = Field.get_datatype(i) + data = { + "field_name": i.field_name, + "label": i.label, + "type": i.type, + "number": i.number, + "value": value + } + # append the fields to array + # use copy() to avoid overwriting + field_set["fields"].append(data.copy()) + + return field_set -class Detail(generics.RetrieveUpdateDestroyAPIView): - queryset = Report.objects.all() - serializer_class = ReportSerializer # API Endpoints - @api_view(['POST']) def report(request): ''' Generate a new empty report and return it ''' - data = { - "title": "2018 Portland trip", - "date_created": "2018-05-22T14:56:28.000Z", - "submitted": False, - "date_submitted": "0000-00-00T00:00:00.000Z", - "sections": [ - { - "id": 1, - "completed": True, - "title": "Flight Info", - "html_description": "

Enter flight details here.

", - "fields": { - "international": { - "label": "International flight", - "type": "boolean", - "value": True - }, - "travel_date": { - "label": "Travel start date", - "type": "date", - "value": "2016-05-22T14:56:28.000Z" - }, - "fare": { - "label": "Fare", - "type": "decimal", - "value": "1024.99" - }, - "lowest_fare_screenshot": { - "label": "Lowest fare screenshot", - "type": "file", - "value": "e92h842jiu49f8..." - }, - "plane_ticket_invoice": { - "label": "Plane ticket invoice PDF", - "type": "file", - "value": "" - } - }, - "rule_violations": [ - { - "error_text": "Plane ticket invoice must be submitted." - } - ] - }, - { - "id": 2, - "completed": False, - "title": "Hotel info", - "html_description": "

If you used a hotel, please enter the details.

", - "fields": { - "total": { - "label": "Total cost", - "type": "decimal" - } - }, - "rule_violations": [ - ] - } - ] - } + + # Create the report + report = Report.objects.create(user_id=request.user, title=request.data['title'], + date_created=datetime.date.today()) + report.save() + + # Create the sections + for i in range(len(pol.sections)): + section = pol.sections[i] + s = Section.objects.create(report_id=report, auto_submit=section.auto_submit, + required=section.required, completed=False, + title=section.title, html_description=section.html_description, + number=i) + s.save() + + # Create the fields + j = 0 + for key in section.fields: + field = section.fields[key] + f = Field.objects.create(section_id=s, field_name=key, label=field['label'], + number=j, type=field['type'], completed=False) + f.save() + j = j+1 + + # Return the newly created report + data = get_reports(report.id) return JsonResponse(data) +# View the list of reports @api_view(['GET']) def reports(request): - data = { - "reports": [ - { - "report_pk": 1, - "title": "2018 Portland trip", - "date_created": "2018-05-22T14:56:28.000Z", - "state": "created", - "date_submitted": "0000-00-00T00:00:00.000Z" - }, - { - "report_pk": 2, - "title": "2017 Los Angeles trip", - "date_created": "2017-05-22T14:56:28.000Z", - "state": "submitted", - "date_submitted": "2017-07-22T14:56:28.000Z" - }, - { - "report_pk": 3, - "title": "2017 Denver trip", - "date_created": "2015-04-22T14:56:28.000Z", - "state": "accepted", - "date_submitted": "2015-06-22T14:56:28.000Z" - } - ] - } - return JsonResponse(data) + report_set = {"reports": []} + queryset = Report.objects.all().filter(user_id=request.user.id).order_by('date_created') + for i in queryset: + data = { + "user_id": request.user.id, + "report_pk": i.id, + "title": i.title, + "date_created": i.date_created, + "submitted": i.submitted, + "date_submitted": i.date_submitted, + } + # append the sections for each report + report_set["reports"].append(data.copy()) + return JsonResponse(report_set) + + +# actions for an individual report @api_view(['GET', 'PUT', 'DELETE']) def report_detail(request, report_pk): + # view the report if request.method == 'GET': - data = { - "report_pk": report_pk, - "title": "2018 Portland trip", - "date_created": "2018-05-22T14:56:28.000Z", - "submitted": False, - "date_submitted": "0000-00-00T00:00:00.000Z", - "sections": [ - { - "id": 1, - "completed": True, - "title": "Flight Info", - "html_description": "

Enter flight details here.

", - "fields": { - "international": { - "label": "International flight", - "type": "boolean", - "value": True - }, - "travel_date": { - "label": "Travel start date", - "type": "date", - "value": "2016-05-22T14:56:28.000Z" - }, - "fare": { - "label": "Fare", - "type": "decimal", - "value": "1024.99" - }, - "lowest_fare_screenshot": { - "label": "Lowest fare screenshot", - "type": "file", - "value": "e92h842jiu49f8..." - }, - "plane_ticket_invoice": { - "label": "Plane ticket invoice PDF", - "type": "file", - "value": "" - } - }, - "rule_violations": [ - { - "error_text": "Plane ticket invoice must be submitted." - } - ] - }, - { - "id": 2, - "completed": False, - "title": "Hotel info", - "html_description": "

If you used a hotel, please enter the details.

", - "fields": { - "total": { - "label": "Total cost", - "type": "decimal" - } - }, - "rule_violations": [ - ] - } - ] - } + data = get_reports(report_pk) return JsonResponse(data) + + # submit the report elif request.method == 'PUT': return JsonResponse({"message": "Report submitted."}) + + # Delete the report elif request.method == 'DELETE': return JsonResponse({"message": "Deleted report {0}.".format(report_pk)}) + +# update a section with new data @api_view(['PUT']) def section(request, report_pk, section_pk): - ''' - Update a section with new data. - ''' data = { "message": "Updated report {0}, section {1}.".format(report_pk, section_pk), "fields": { @@ -198,25 +149,5 @@ def section(request, report_pk, section_pk): "lowest_fare_screenshot": "image", } } + return JsonResponse(data) - -@api_view(['POST']) -def account(request): - ''' - Create a new user account - ''' - return JsonResponse({"message": "Account creation successful."}) - -@api_view(['POST']) -def account_login(request): - ''' - Log in to a user account - ''' - return JsonResponse({"message": "Successfully logged in."}) - -@api_view(['DELETE']) -def account_logout(request): - ''' - Log out from a user account - ''' - return JsonResponse({"message": "User logged out."}) diff --git a/back/db.sqlite3 b/back/db.sqlite3 index bfc38f7..54ace4a 100644 Binary files a/back/db.sqlite3 and b/back/db.sqlite3 differ diff --git a/back/reimbursinator/custom_auth.py b/back/reimbursinator/custom_auth.py new file mode 100644 index 0000000..9097e26 --- /dev/null +++ b/back/reimbursinator/custom_auth.py @@ -0,0 +1,9 @@ +from rest_framework.authentication import TokenAuthentication + +class BearerAuthentication(TokenAuthentication): + """ + This class simply changes the expected token keyword to 'Bearer' + from the Django rest authentication default 'Token'. This allows + applications like Postman to work with token authentication. + """ + keyword = "Bearer" diff --git a/back/reimbursinator/settings.py b/back/reimbursinator/settings.py index e274dc5..86f1b58 100644 --- a/back/reimbursinator/settings.py +++ b/back/reimbursinator/settings.py @@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/2.1/ref/settings/ """ import os +#from reimbursinator.custom_auth import BearerAuthentication # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -38,8 +39,15 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.sites', # 3rd party 'rest_framework', + 'rest_framework.authtoken', + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'rest_auth', + 'rest_auth.registration', 'corsheaders', # local 'users', @@ -48,8 +56,12 @@ INSTALLED_APPS = [ REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.AllowAny', - ] + 'rest_framework.permissions.IsAuthenticated', + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'reimbursinator.custom_auth.BearerAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ], } MIDDLEWARE = [ @@ -142,3 +154,25 @@ USE_TZ = True # https://docs.djangoproject.com/en/2.1/howto/static-files/ STATIC_URL = '/static/' + +# Email Config + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + +SITE_ID = 1 + +# Registration + +#ACCOUNT_USER_MODEL_USERNAME_FIELD = 'email' +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_USERNAME_REQUIRED = False +ACCOUNT_AUTHENTICATION_METHOD = 'email' + +REST_AUTH_REGISTER_SERIALIZERS = { + 'REGISTER_SERIALIZER': 'users.serializers.RegisterSerializer', +} + +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', +) diff --git a/back/reimbursinator/urls.py b/back/reimbursinator/urls.py index 53beb58..7a33b6d 100644 --- a/back/reimbursinator/urls.py +++ b/back/reimbursinator/urls.py @@ -12,4 +12,8 @@ from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/v1/', include("backend.urls")), -] \ No newline at end of file + path('api/v1/account/', include('rest_auth.urls')), + path('api/v1/account/register/', include('rest_auth.registration.urls')), + # path('api/v1/account/register/', NameRegistrationView.as_view()), + path('api-auth/', include('rest_framework.urls')), +] diff --git a/back/uploads/2019/01/31/CUqADRaW4AAb1QI.jpg_large.jpg b/back/uploads/2019/01/31/CUqADRaW4AAb1QI.jpg_large.jpg deleted file mode 100644 index 151c5b5..0000000 Binary files a/back/uploads/2019/01/31/CUqADRaW4AAb1QI.jpg_large.jpg and /dev/null differ diff --git a/back/uploads/2019/02/09/Supreme-logo-newyork-1920x1080.jpg b/back/uploads/2019/02/09/Supreme-logo-newyork-1920x1080.jpg new file mode 100644 index 0000000..1f7922a Binary files /dev/null and b/back/uploads/2019/02/09/Supreme-logo-newyork-1920x1080.jpg differ diff --git a/back/users/__init__.py b/back/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/back/users/serializers.py b/back/users/serializers.py new file mode 100644 index 0000000..91416a4 --- /dev/null +++ b/back/users/serializers.py @@ -0,0 +1,47 @@ +from rest_framework import serializers +from allauth.account import app_settings as allauth_settings +from allauth.utils import email_address_exists +from allauth.account.adapter import get_adapter +from allauth.account.utils import setup_user_email +from django.utils.translation import gettext as _ + +class RegisterSerializer(serializers.Serializer): + email = serializers.EmailField(required=allauth_settings.EMAIL_REQUIRED) + first_name = serializers.CharField(required=True, write_only=True) + last_name = serializers.CharField(required=True, write_only=True) + password1 = serializers.CharField(required=True, write_only=True) + password2 = serializers.CharField(required=True, write_only=True) + + def validate_email(self, email): + email = get_adapter().clean_email(email) + if allauth_settings.UNIQUE_EMAIL: + if email and email_address_exists(email): + raise serializers.ValidationError( + _("A user is already registered with this e-mail address.")) + return email + + def validate_password1(self, password): + return get_adapter().clean_password(password) + + def validate(self, data): + if data['password1'] != data['password2']: + raise serializers.ValidationError( + _("The two password fields didn't match.")) + return data + + def get_cleaned_data(self): + return { + 'first_name': self.validated_data.get('first_name', ''), + 'last_name': self.validated_data.get('last_name', ''), + 'password1': self.validated_data.get('password1', ''), + 'email': self.validated_data.get('email', ''), + } + + def save(self, request): + adapter = get_adapter() + user = adapter.new_user(request) + self.cleaned_data = self.get_cleaned_data() + adapter.save_user(request, user, self) + setup_user_email(request, user, []) + user.save() + return user diff --git a/back/users/views.py b/back/users/views.py index 91ea44a..2536b37 100644 --- a/back/users/views.py +++ b/back/users/views.py @@ -1,3 +1 @@ from django.shortcuts import render - -# Create your views here. diff --git a/front/static/edit_report.html b/front/static/edit_report.html index 5c7afc8..2714d66 100644 --- a/front/static/edit_report.html +++ b/front/static/edit_report.html @@ -36,7 +36,7 @@ -
+
@@ -81,6 +81,20 @@
+ diff --git a/front/static/home.html b/front/static/home.html index 114887d..99d984c 100644 --- a/front/static/home.html +++ b/front/static/home.html @@ -35,7 +35,7 @@
-
+

Welcome to Reimbursinator

diff --git a/front/static/index.html b/front/static/index.html index 713bb3a..f759c1b 100644 --- a/front/static/index.html +++ b/front/static/index.html @@ -27,7 +27,7 @@
-
+

Reimbursinator

An open source expense management solution sponsored by the Software Freedom Conservancy

diff --git a/front/static/js/login.js b/front/static/js/login.js index 8269c99..0bdaa9b 100644 --- a/front/static/js/login.js +++ b/front/static/js/login.js @@ -1,18 +1,14 @@ -function displayErrorMessage(errorMessage) { - const errorReport = document.querySelector("#errorReport"); - errorReport.innerHTML = JSON.parse(errorMessage).error; -} - function postToLoginEndpoint(event) { event.preventDefault(); const credentials = { - "username" : this.elements.username.value, + "email" : this.elements.email.value, "password" : this.elements.password.value } - const url = "https://reqres.in/api/login" // mock api service + const url = "https://" + window.location.hostname + ":8444/api/v1/account/login/"; const xhr = new XMLHttpRequest(); + console.log("Attempting a connection to the following endpoint: " + url); console.log("User credentials:\n" + JSON.stringify(credentials)); xhr.open("POST", url, true); @@ -22,14 +18,14 @@ function postToLoginEndpoint(event) { if (this.status === 200) { console.log("LOGIN SUCCESS!"); console.log("Server response:\n" + this.response); - token = JSON.parse(this.response).token; + token = JSON.parse(this.response).key; localStorage.setItem("token", token); window.location.replace("home.html"); } else { + document.getElementById("errorLogin").innerHTML = "Incorrect user name or password"; console.error("LOGIN FAILURE!"); console.error("Server status: " + this.status); console.error("Server response:\n" + this.response); - displayErrorMessage(this.response); } } }; diff --git a/front/static/js/logout.js b/front/static/js/logout.js index c10c381..b3bef0d 100644 --- a/front/static/js/logout.js +++ b/front/static/js/logout.js @@ -2,18 +2,18 @@ function postToLogoutEndpoint(event) { event.preventDefault(); const token = localStorage.getItem("token"); - const url = "https://reqres.in/api/logout" // mock api service + const url = "https://" + window.location.hostname + ":8444/api/v1/account/logout/"; const xhr = new XMLHttpRequest(); xhr.open("POST", url, true); - xhr.setRequestHeader("Authorization", "Token " + token); + xhr.setRequestHeader("Authorization", "Bearer " + token); xhr.onreadystatechange = function() { if (this.readyState === 4) { if (this.status === 200) { console.log("LOGOUT SUCCESS!"); console.log("Server response:\n" + this.response); localStorage.removeItem("token"); - window.location.replace("index.html"); + window.location.replace("/"); } else { console.error("LOGOUT FAILURE!"); console.error("Server status: " + this.status); diff --git a/front/static/js/signupPage.js b/front/static/js/signupPage.js index 0055cce..8777450 100644 --- a/front/static/js/signupPage.js +++ b/front/static/js/signupPage.js @@ -1,20 +1,64 @@ -const password = document.getElementById("password"); -const confirm_password = document.getElementById("confirmPassword"); -function validatePassword(){ - if(password.value != confirm_password.value) { - confirm_password.setCustomValidity("Passwords Don't Match"); - } - else { - confirm_password.setCustomValidity(''); +const password1 = document.getElementById("password1"); +const password2 = document.getElementById("password2"); + +function validatePassword() { + if (password1.value != password2.value) { + password2.setCustomValidity("Passwords don't match"); + } else { + password2.setCustomValidity(''); } } -password.onchange = validatePassword; -confirm_password.onkeyup = validatePassword; +password1.onchange = validatePassword; +password2.onkeyup = validatePassword; -function validateEmail(email) -{ - if(email.validity.patternMismatch) +function validateEmail(email) { + if (email.validity.patternMismatch) { email.setCustomValidity('Please input correct email'); - else + } else { email.setCustomValidity(''); -} \ No newline at end of file + } +} + +function postToRegistrationEndpoint(event) { + event.preventDefault(); + + const credentials = { + "email" : this.elements.email.value, + "first_name" : this.elements.first_name.value, + "last_name" : this.elements.last_name.value, + "password1" : this.elements.password1.value, + "password2" : this.elements.password2.value + } + const url = "https://" + window.location.hostname + ":8444/api/v1/account/register/"; + const xhr = new XMLHttpRequest(); + + console.log("Attempting a connection to the following endpoint: " + url); + console.log("User credentials:\n" + JSON.stringify(credentials)); + + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.onreadystatechange = function() { + if (this.readyState === 4) { + if (this.status === 201) { + console.log("REGISTRATION SUCCESS!"); + console.log("Server response:\n" + this.response); + token = JSON.parse(this.response).key; + localStorage.setItem("token", token); + window.location.replace("home.html"); + } else { + console.error("REGISTRATION FAILURE!"); + console.error("Server status: " + this.status); + console.error("Server response:\n" + this.response); + } + } + }; + + xhr.onerror = function() { + alert("Error connecting to the authentication server!"); + }; + + xhr.send(JSON.stringify(credentials)); +} + +const form = document.querySelector("form"); +form.addEventListener("submit", postToRegistrationEndpoint); diff --git a/front/static/js/viewHistory.js b/front/static/js/viewHistory.js index a6b61d2..18af135 100644 --- a/front/static/js/viewHistory.js +++ b/front/static/js/viewHistory.js @@ -11,6 +11,7 @@ function getDataFromEndpoint(url, callback, optional) { console.log("Attempting a connection to the following endpoint: " + url); xhr.open("GET", url, true); + xhr.setRequestHeader("Authorization", "Bearer " + token); xhr.onreadystatechange = function() { if (this.readyState === 4) { if (this.status === 200) { @@ -33,22 +34,55 @@ function getDataFromEndpoint(url, callback, optional) { xhr.send(); } + +// Make a POST request to url and pass response to callback function +function postDataToEndpoint(url, payload, callback, optional) { + const token = localStorage.getItem("token"); + const xhr = new XMLHttpRequest(); + + console.log("Attempting a connection to the following endpoint: " + url); + + xhr.open("POST", url, true); + xhr.setRequestHeader("Authorization", "Bearer " + token); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.onreadystatechange = function() { + if (this.readyState === 4) { + if (this.status === 200) { + console.log("POST SUCCESS!"); + console.log("Server response:\n" + this.response); + let parsedData = JSON.parse(this.response); + optional === undefined ? callback(parsedData) : callback(parsedData, optional); + } else { + console.error("POST FAILURE!"); + console.error("Server status: " + this.status); + console.error("Server response:\n" + this.response); + } + } + }; + + xhr.onerror = function() { + alert("Connection error!"); + }; + + xhr.send(payload); +} + // Wraps a Bootstrap form group around a field -function createFormGroup(key, field) { +function createFormGroup(field) { const formGroup = document.createElement("div") formGroup.classList.add("form-group", "row"); const label = document.createElement("label"); label.classList.add("col-sm-4", "col-form"); label.innerHTML = field.label + ": "; - label.setAttribute("for", key); + label.setAttribute("for", field.field_name); const div = document.createElement("div"); div.classList.add("col-sm-6"); const input = document.createElement("input"); - input.name = key; - input.id = key; + input.name = field.field_name; + input.id = field.field_name; switch(field.type) { case "boolean": @@ -58,6 +92,7 @@ function createFormGroup(key, field) { input.classList.add("form-check-input"); label.className = ""; label.classList.add("form-check-label"); + label.innerHTML = field.label; outerLabel = document.createElement("div"); outerLabel.classList.add("col-sm-4"); outerLabel.innerHTML = "Flight type: "; @@ -70,10 +105,29 @@ function createFormGroup(key, field) { formGroup.appendChild(div); break; case "date": + case "string": + input.type = "text"; + input.value = field.value; + input.classList.add("form-control"); + formGroup.appendChild(label); + div.appendChild(input) + formGroup.appendChild(div); + break; case "decimal": input.type = "text"; input.value = field.value; input.classList.add("form-control"); + input.pattern = "\\d+(\\.\\d{2})?"; + formGroup.appendChild(label); + div.appendChild(input) + formGroup.appendChild(div); + break; + case "integer": + input.type = "number"; + input.value = field.value; + input.classList.add("form-control"); + input.step = 1; + input.min = 0; formGroup.appendChild(label); div.appendChild(input) formGroup.appendChild(div); @@ -158,10 +212,12 @@ function createReportForm(parsedData, type) { accordion.classList.add("accordion"); if (type === reportType.EDIT) { + console.log("reportType.EDIT"); modalBody = document.querySelector("#editReportModalBody"); modalLabel = document.querySelector("#editReportModalLabel"); accordion.id = "editReportAccordion"; } else if (type === reportType.NEW) { + console.log("reportType.NEW"); modalBody = document.querySelector("#newReportModalBody"); modalLabel = document.querySelector("#newReportModalLabel"); accordion.id = "newReportAccordion"; @@ -199,7 +255,7 @@ function createReportForm(parsedData, type) { console.log("Field value: " + field.value); // Create a form group for each field and add it to the form - let formGroup = createFormGroup(key, field); + let formGroup = createFormGroup(field); form.appendChild(formGroup); } @@ -235,7 +291,7 @@ function displayListOfReports(parsedData) { for (let i = 0; i < reports.length; i++) { let title = reports[i].title; let dateCreated = new Date(reports[i].date_created).toLocaleDateString("en-US"); - let state = reports[i].state; + let state = reports[i].submitted; let dateSubmitted; let rid = reports[i].report_pk; @@ -253,7 +309,7 @@ function displayListOfReports(parsedData) { actionButton.setAttribute("data-rid", rid); actionButton.classList.add("btn"); - if (state === "created") { + if (state === false) { // Edit button dateSubmitted = "TBD"; actionButton.classList.add("btn-primary", "edit-report-button"); // Add event listener class @@ -263,8 +319,10 @@ function displayListOfReports(parsedData) { } else { // View button dateSubmitted = new Date(reports[i].date_submitted).toLocaleDateString("en-US"); - actionButton.classList.add("btn-success"); + actionButton.classList.add("btn-success", "view-report-button"); actionButton.innerHTML = "View"; + actionButton.setAttribute("data-toggle", "modal"); + actionButton.setAttribute("data-target", "#viewReportModal"); } let dateSubmittedCell = bodyRow.insertCell(3); @@ -277,6 +335,63 @@ function displayListOfReports(parsedData) { } } +function displayReport(parsedData){ + //Able to get the correct report ID now just needs to display the + //report as an modual + const modalBody = document.querySelector(".modal-view"); + const modalLabel = document.querySelector("#viewReportModalLabel"); + + while (modalBody.firstChild) { + modalBody.removeChild(modalBody.firstChild); + } + + // Add report title and date + const reportTitle = parsedData.title; + const dateCreated = new Date(parsedData.date_created).toLocaleDateString("en-US"); + modalLabel.innerHTML = reportTitle + " " + dateCreated; + + const card = document.createElement("div"); + card.classList.add("card"); + + const cardHeader = document.createElement("div"); + cardHeader.classList.add("card-header"); + + const cardBody = document.createElement("div"); + cardBody.classList.add("card-body"); + + /* + const displayTable = document.createElement("table"); + displayTable.classList.add("table table-striped table-responsive-sm"); + displayTable.style.visibility = "visible"; + cardBody.appendChild(displayTable); +*/ + + + const sections = parsedData.sections; + for (let key in sections) { + let section = sections[key]; + if(section.completed) { + const h4 = document.createElement("h4"); + const value = document.createTextNode(section.title); + + h4.appendChild(value); + cardBody.appendChild(h4); + let fields = section.fields; + for (let key in fields) { + let field = fields[key]; + const p1 = document.createElement("p"); + const p1Value = document.createTextNode(field.label + ": " + field.value); + p1.appendChild(p1Value); + cardBody.appendChild(p1); + } + cardHeader.appendChild(cardBody); + card.appendChild(cardHeader); + } + } + + modalBody.appendChild(card); +} + document.addEventListener("DOMContentLoaded", function(event) { if (window.location.pathname === "/edit_report.html") { const url = getEndpointDomain() + "api/v1/reports"; @@ -285,9 +400,9 @@ document.addEventListener("DOMContentLoaded", function(event) { }); const reportType = { - EDIT : 1, - VIEW : 2, - NEW : 3 + NEW : 1, + EDIT : 2, + VIEW : 3 }; document.addEventListener("click", function(event) { @@ -298,13 +413,21 @@ document.addEventListener("click", function(event) { const url = getEndpointDomain() + "api/v1/report/" + event.target.dataset.rid; const type = reportType.EDIT; getDataFromEndpoint(url, createReportForm, type); - } else if (event.target.classList.contains("new-report-button")) { - //const url = getEndpointDomain() + "api/v1/report"; - const type = reportType.NEW; - //getDataFromEndpoint(url, createReportForm, type); - createReportForm(newReport, type); + } else if (event.target.classList.contains("view-report-button")) { + console.log("View button clicked"); + const url = getEndpointDomain() + "api/v1/report/" + event.target.dataset.rid; + getDataFromEndpoint(url, displayReport); } } - - // TODO: Add view report +}); + +document.addEventListener("submit", function(event) { + event.preventDefault(); + if (event.target.classList.contains("new-report")) { + const url = getEndpointDomain() + "api/v1/report"; + const payload = JSON.stringify({ "title": event.target.elements.title.value }); + console.log("Payload:\n" + payload); + const type = reportType.NEW; + postDataToEndpoint(url, payload, createReportForm, type); + } }); diff --git a/front/static/login.html b/front/static/login.html index 5c60531..a09051e 100644 --- a/front/static/login.html +++ b/front/static/login.html @@ -14,7 +14,7 @@ -
+
@@ -24,13 +24,14 @@
- - + +
- - + +
+

@@ -41,7 +42,6 @@
-

diff --git a/front/static/new_report.html b/front/static/new_report.html index 8a57c71..40189c0 100644 --- a/front/static/new_report.html +++ b/front/static/new_report.html @@ -36,8 +36,25 @@

-
- +
+
+
+
+
+

Create a new report

+
+
+
+
+ + +
+ +
+
+
+
+
- diff --git a/front/static/signup.html b/front/static/signup.html index ebfeb1b..12b1119 100644 --- a/front/static/signup.html +++ b/front/static/signup.html @@ -14,7 +14,7 @@ -
+
@@ -23,21 +23,25 @@
@@ -51,5 +55,4 @@
-