Merge branch 'master' into display-rule-violations

This commit is contained in:
Preston Doman 2019-02-16 20:33:19 -08:00
commit 69959a3510
5 changed files with 160 additions and 45 deletions

View file

@ -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)

View file

@ -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(

View file

@ -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)

View file

@ -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)

View file

@ -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)