Merge branch 'master' into display-rule-violations
This commit is contained in:
commit
69959a3510
5 changed files with 160 additions and 45 deletions
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue