diff --git a/back/backend/models.py b/back/backend/models.py index e52b763..7859a08 100644 --- a/back/backend/models.py +++ b/back/backend/models.py @@ -4,6 +4,10 @@ import datetime import ntpath class Report(models.Model): + """ + This model represents an expense report that can be + created, updated and submitted by a user. + """ user_id = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) title = models.CharField(max_length=128) date_created = models.DateTimeField('date created', default=datetime.date.today) @@ -11,9 +15,17 @@ class Report(models.Model): submitted = models.BooleanField(default=False) def __str__(self): + """ + For debugging and display in admin view. + """ return self.title class Section(models.Model): + """ + This model represents a logical division of a report, + containing its own fields and rules that apply to those + fields. + """ report_id = models.ForeignKey(Report, on_delete=models.CASCADE) auto_submit = models.BooleanField(default=False) required = models.BooleanField(default=False) @@ -23,9 +35,18 @@ class Section(models.Model): number = models.IntegerField() def __str__(self): + """ + For debugging and display in admin view. + """ return "{0}(#{1})".format(self.title, self.number) class Field(models.Model): + """ + This model contains a piece of data entered by the user. + Depending on the type of the data ( boolean, decimal, + date, file, string or integer), different table columns + will be used to store the data. + """ section_id = models.ForeignKey(Section, on_delete=models.CASCADE) field_name = models.CharField(max_length=512, default="field") label = models.CharField(max_length=512) @@ -39,9 +60,10 @@ class Field(models.Model): 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): + """ + For debugging and display in the admin view. + """ if self.field_type == "boolean": if self.data_bool: return "True" @@ -58,10 +80,11 @@ class Field(models.Model): elif self.field_type == "integer": return "{}".format(self.data_integer) - - # function that gets corresponding - # data type def get_datatype(self): + """ + Returns the data corresponding to the type of the + field. + """ if self.field_type == "boolean": if self.data_bool: return True @@ -79,8 +102,9 @@ class Field(models.Model): elif self.field_type == "integer": return self.data_integer - # function that accommodates if - # path has slash at end def path_leaf(self, path): + """ + Function accommodating path with slash at end. + """ dir_path, name = ntpath.split(path) return name or ntpath.basename(dir_path) diff --git a/back/backend/policy.py b/back/backend/policy.py index 723a76b..9808475 100644 --- a/back/backend/policy.py +++ b/back/backend/policy.py @@ -1,19 +1,42 @@ from datetime import date -#### Classes for policy, sections. +#### Classes for policy, sections. Do not edit these. +##################################################### class Policy(): - + """ + Represents the policy for the company/organization. + """ def __init__(self): + """ + Creates a new Policy object. + """ self.sections = [] def add_section(self, section): + """ + Appends the specified section to the policy in order. + + section -- Section object to append. + """ self.sections.append(section) class Section(): - - def __init__(self, title="Section", html_description="", required=False, - auto_submit=False, fields={}): + """ + Represents a logical division of te policy, containing + data fields for users to enter, and rules which + apply to those fields. + """ + def __init__(self, title="Section", html_description="", required=False, auto_submit=False, fields={}): + """ + Creates a new Section object. + + title -- This is the name for the section the user sees. + html_description -- This is html displayed beneath the title. + required -- If True, user must complete before submitting. + auto_submit -- Completing this section notifies the administrator. + fields -- A python object of fields the user should enter. + """ self.title = title self.html_description = html_description self.required = required @@ -22,6 +45,13 @@ class Section(): self.rules = [] def add_rule(self, title="Rule", rule=None, rule_break_text=""): + """ + Assigns a new rule to the section. + + title -- Administrative title for the rule. + rule -- lambda expression which must evaluate true to pass the rule. + rule_break_text -- Error message to show the user when rule is broken. + """ rule = { "title": title, "rule": rule, @@ -29,10 +59,11 @@ class Section(): } self.rules.append(rule) -#### Policy configuration begin here - pol = Policy() +#### Policy configuration begins here. Edit below this line. +############################################################ + #### General #### Section 0 general_section = Section( diff --git a/back/backend/views.py b/back/backend/views.py index a34a661..b01e348 100644 --- a/back/backend/views.py +++ b/back/backend/views.py @@ -6,9 +6,12 @@ import os from rest_framework.exceptions import ParseError from rest_framework.parsers import FileUploadParser, MultiPartParser - -# function that prints all the reports -def get_reports(report_pk): +def get_report(report_pk): + """ + Returns a python object representation of the specified section. + + report_pk -- ID of the report to compile. + """ queryset = Report.objects.filter(id=report_pk) for i in queryset: data = { @@ -24,9 +27,12 @@ def get_reports(report_pk): # return JsonResponse(data) return data -# function that gets all the sections -# takes report_id param def get_sections(r_id): + """ + Returns a python object array of sections for the specified report. + + r_id -- ID of the report to compile sections for. + """ # create a dict of arrays for section section_set = {"sections": []} queryset = Section.objects.filter(report_id=r_id) @@ -60,9 +66,12 @@ def get_sections(r_id): return section_set -# function that gets all the fields -# takes section_id param def get_fields(s_id): + """ + Returns a python object array of fields for the specified section. + + s_id -- ID of the section to compile fields for. + """ # create dict of arrays for fields field_set = {"fields": []} queryset = Field.objects.filter(section_id=s_id).order_by('number') @@ -87,10 +96,12 @@ def get_fields(s_id): def generate_named_fields_for_section(fields): - ''' - Converts a section's field data into key-value pairs - for use in policy rule lambda functions. - ''' + """ + Prepares a dictionary of key-value pairs based on the raw fields + passed in. Used to pass into the rule lambda functions. + + fields -- Python object prepared by get_fields + """ result = {} for field in fields: key = field['field_name'] @@ -98,13 +109,16 @@ def generate_named_fields_for_section(fields): result[key] = value return result -# API Endpoints @api_view(['POST']) def report(request): - ''' - Generate a new empty report and return it - ''' - + """ + Generates a new empty report for the current user and returns it + in json format. The title of the report should be provided as + follows: + { + "title": "Report Title Here" + } + """ # Create the report report = Report.objects.create(user_id=request.user, title=request.data['title'], date_created=datetime.date.today()) @@ -127,12 +141,15 @@ def report(request): f.save() # Return the newly created report - data = get_reports(report.id) + data = get_report(report.id) return JsonResponse(data) -# View the list of reports @api_view(['GET']) def reports(request): + """ + Returns a condensed version of the current user's reports in json + format. + """ report_set = {"reports": []} queryset = Report.objects.all().filter(user_id=request.user.id).order_by('date_created') for i in queryset: @@ -150,31 +167,40 @@ def reports(request): return JsonResponse(report_set) def user_owns_report(user, report): - ''' - Returns true if the specified user is owner of the report - ''' + """ + Returns true if the specified user is owner of the report. + + report -- ID of the report to check. + """ report_to_check = Report.objects.filter(id=report) if len(report_to_check) < 1: return False return report_to_check[0].user_id == user -# actions for an individual report @api_view(['GET', 'PUT', 'DELETE']) def report_detail(request, report_pk): + """ + Handler for individual report actions. Actions are divided into + GET, PUT, and DELETE requests. + + report_pk -- ID of the report to carry out the action on. + """ # Check that the user owns the report if not user_owns_report(user=request.user, report=report_pk): return JsonResponse({"message": "Current user does not own the specified report."}, status=401) - # view the report + # GET: Retrieves a json representation of the specified report if request.method == 'GET': - data = get_reports(report_pk) + data = get_report(report_pk) return JsonResponse(data) - # submit the report + # PUT: Submits a report to the administrator for review, + # and marks it as "submitted", after which changes may + # not be made. elif request.method == 'PUT': return JsonResponse({"message": "Report submitted."}) - # Delete the report + # DELETE: Deletes a report from the user's account. elif request.method == 'DELETE': # get corresponding sections section_set = Section.objects.filter(report_id=report_pk) @@ -193,18 +219,24 @@ def report_detail(request, report_pk): return JsonResponse({"message": "Deleted report: {0}.".format(title)}) def user_owns_section(user, section): - ''' - Returns true if the specified user is owner of the section - ''' + """ + Returns true if the specified user is owner of the section. + + section -- ID of the section to check. + """ section_to_check = Section.objects.filter(id=section) if len(section_to_check) < 1: return False report_to_check = section_to_check[0].report_id return report_to_check.user_id == user -# update a section with new data @api_view(['PUT']) def section(request, report_pk, section_pk): + """ + Updates the specified section with new data. + + section_pk -- Section for which the data should be updated. + """ # Check that the user owns the report if not user_owns_section(user=request.user, section=section_pk): return JsonResponse({"message": "Current user does not own the specified section."}, status=401) @@ -288,12 +320,33 @@ def section(request, report_pk, section_pk): "completed": s.completed, "title": s.title, "html_description": s.html_description, + "rule_violations": [], } data.update(get_fields(s.id)) + # process rules from the policy file if the section is completed + if s.completed: + rules = pol.sections[s.number].rules + for rule in rules: + try: + named_fields = generate_named_fields_for_section(data['fields']) + if not rule['rule'](data, named_fields): + info = { + "label": rule['title'], + "rule_break_text": rule['rule_break_text'], + } + data['rule_violations'].append(info) + except Exception as e: + print('Rule "{}" encountered an error. {}'.format(rule['title'], e)) return JsonResponse(data) -# function checks if a field is complete def section_complete(section_pk): + """ + Returns True if any fields of the specified section have been + entered by the user. This means that entering even one field + will count the entire section as "complete". + + section_pk -- ID of the section whose fields you wish to check. + """ # grab field set check_fields = Field.objects.filter(section_id=section_pk) diff --git a/back/users/models.py b/back/users/models.py index e51d043..aa29bc6 100644 --- a/back/users/models.py +++ b/back/users/models.py @@ -2,4 +2,7 @@ from django.contrib.auth.models import AbstractUser from django.db import models class CustomUser(AbstractUser): + """ + Custom user model to allow for new fields if necessary. + """ age = models.PositiveIntegerField(null=True, blank=True) diff --git a/back/users/serializers.py b/back/users/serializers.py index 91416a4..8cbc2fa 100644 --- a/back/users/serializers.py +++ b/back/users/serializers.py @@ -6,6 +6,10 @@ from allauth.account.utils import setup_user_email from django.utils.translation import gettext as _ class RegisterSerializer(serializers.Serializer): + """ + Custom serializer to allow users to register with only + their email address, first and last name, and password. + """ 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)