Merge pull request #87 from danieldupriest/add-rules-to-section
Added rules to section and comment backend code
This commit is contained in:
commit
a18ead6f7f
5 changed files with 160 additions and 45 deletions
|
@ -4,6 +4,10 @@ import datetime
|
||||||
import ntpath
|
import ntpath
|
||||||
|
|
||||||
class Report(models.Model):
|
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)
|
user_id = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
title = models.CharField(max_length=128)
|
title = models.CharField(max_length=128)
|
||||||
date_created = models.DateTimeField('date created', default=datetime.date.today)
|
date_created = models.DateTimeField('date created', default=datetime.date.today)
|
||||||
|
@ -11,9 +15,17 @@ class Report(models.Model):
|
||||||
submitted = models.BooleanField(default=False)
|
submitted = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
"""
|
||||||
|
For debugging and display in admin view.
|
||||||
|
"""
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
class Section(models.Model):
|
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)
|
report_id = models.ForeignKey(Report, on_delete=models.CASCADE)
|
||||||
auto_submit = models.BooleanField(default=False)
|
auto_submit = models.BooleanField(default=False)
|
||||||
required = models.BooleanField(default=False)
|
required = models.BooleanField(default=False)
|
||||||
|
@ -23,9 +35,18 @@ class Section(models.Model):
|
||||||
number = models.IntegerField()
|
number = models.IntegerField()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
"""
|
||||||
|
For debugging and display in admin view.
|
||||||
|
"""
|
||||||
return "{0}(#{1})".format(self.title, self.number)
|
return "{0}(#{1})".format(self.title, self.number)
|
||||||
|
|
||||||
class Field(models.Model):
|
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)
|
section_id = models.ForeignKey(Section, on_delete=models.CASCADE)
|
||||||
field_name = models.CharField(max_length=512, default="field")
|
field_name = models.CharField(max_length=512, default="field")
|
||||||
label = models.CharField(max_length=512)
|
label = models.CharField(max_length=512)
|
||||||
|
@ -39,9 +60,10 @@ class Field(models.Model):
|
||||||
data_string = models.TextField(default='', blank=True)
|
data_string = models.TextField(default='', blank=True)
|
||||||
data_integer = models.IntegerField(default=0, blank=True)
|
data_integer = models.IntegerField(default=0, blank=True)
|
||||||
|
|
||||||
# function that prints the string representation
|
|
||||||
# on the api?
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
"""
|
||||||
|
For debugging and display in the admin view.
|
||||||
|
"""
|
||||||
if self.field_type == "boolean":
|
if self.field_type == "boolean":
|
||||||
if self.data_bool:
|
if self.data_bool:
|
||||||
return "True"
|
return "True"
|
||||||
|
@ -58,10 +80,11 @@ class Field(models.Model):
|
||||||
elif self.field_type == "integer":
|
elif self.field_type == "integer":
|
||||||
return "{}".format(self.data_integer)
|
return "{}".format(self.data_integer)
|
||||||
|
|
||||||
|
|
||||||
# function that gets corresponding
|
|
||||||
# data type
|
|
||||||
def get_datatype(self):
|
def get_datatype(self):
|
||||||
|
"""
|
||||||
|
Returns the data corresponding to the type of the
|
||||||
|
field.
|
||||||
|
"""
|
||||||
if self.field_type == "boolean":
|
if self.field_type == "boolean":
|
||||||
if self.data_bool:
|
if self.data_bool:
|
||||||
return True
|
return True
|
||||||
|
@ -79,8 +102,9 @@ class Field(models.Model):
|
||||||
elif self.field_type == "integer":
|
elif self.field_type == "integer":
|
||||||
return self.data_integer
|
return self.data_integer
|
||||||
|
|
||||||
# function that accommodates if
|
|
||||||
# path has slash at end
|
|
||||||
def path_leaf(self, path):
|
def path_leaf(self, path):
|
||||||
|
"""
|
||||||
|
Function accommodating path with slash at end.
|
||||||
|
"""
|
||||||
dir_path, name = ntpath.split(path)
|
dir_path, name = ntpath.split(path)
|
||||||
return name or ntpath.basename(dir_path)
|
return name or ntpath.basename(dir_path)
|
||||||
|
|
|
@ -1,19 +1,42 @@
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
#### Classes for policy, sections.
|
#### Classes for policy, sections. Do not edit these.
|
||||||
|
#####################################################
|
||||||
|
|
||||||
class Policy():
|
class Policy():
|
||||||
|
"""
|
||||||
|
Represents the policy for the company/organization.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Creates a new Policy object.
|
||||||
|
"""
|
||||||
self.sections = []
|
self.sections = []
|
||||||
|
|
||||||
def add_section(self, section):
|
def add_section(self, section):
|
||||||
|
"""
|
||||||
|
Appends the specified section to the policy in order.
|
||||||
|
|
||||||
|
section -- Section object to append.
|
||||||
|
"""
|
||||||
self.sections.append(section)
|
self.sections.append(section)
|
||||||
|
|
||||||
class Section():
|
class Section():
|
||||||
|
"""
|
||||||
def __init__(self, title="Section", html_description="", required=False,
|
Represents a logical division of te policy, containing
|
||||||
auto_submit=False, fields={}):
|
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.title = title
|
||||||
self.html_description = html_description
|
self.html_description = html_description
|
||||||
self.required = required
|
self.required = required
|
||||||
|
@ -22,6 +45,13 @@ class Section():
|
||||||
self.rules = []
|
self.rules = []
|
||||||
|
|
||||||
def add_rule(self, title="Rule", rule=None, rule_break_text=""):
|
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 = {
|
rule = {
|
||||||
"title": title,
|
"title": title,
|
||||||
"rule": rule,
|
"rule": rule,
|
||||||
|
@ -29,10 +59,11 @@ class Section():
|
||||||
}
|
}
|
||||||
self.rules.append(rule)
|
self.rules.append(rule)
|
||||||
|
|
||||||
#### Policy configuration begin here
|
|
||||||
|
|
||||||
pol = Policy()
|
pol = Policy()
|
||||||
|
|
||||||
|
#### Policy configuration begins here. Edit below this line.
|
||||||
|
############################################################
|
||||||
|
|
||||||
#### General
|
#### General
|
||||||
#### Section 0
|
#### Section 0
|
||||||
general_section = Section(
|
general_section = Section(
|
||||||
|
|
|
@ -6,9 +6,12 @@ import os
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework.parsers import FileUploadParser, MultiPartParser
|
from rest_framework.parsers import FileUploadParser, MultiPartParser
|
||||||
|
|
||||||
|
def get_report(report_pk):
|
||||||
# function that prints all the reports
|
"""
|
||||||
def get_reports(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)
|
queryset = Report.objects.filter(id=report_pk)
|
||||||
for i in queryset:
|
for i in queryset:
|
||||||
data = {
|
data = {
|
||||||
|
@ -24,9 +27,12 @@ def get_reports(report_pk):
|
||||||
# return JsonResponse(data)
|
# return JsonResponse(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# function that gets all the sections
|
|
||||||
# takes report_id param
|
|
||||||
def get_sections(r_id):
|
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
|
# create a dict of arrays for section
|
||||||
section_set = {"sections": []}
|
section_set = {"sections": []}
|
||||||
queryset = Section.objects.filter(report_id=r_id)
|
queryset = Section.objects.filter(report_id=r_id)
|
||||||
|
@ -60,9 +66,12 @@ def get_sections(r_id):
|
||||||
|
|
||||||
return section_set
|
return section_set
|
||||||
|
|
||||||
# function that gets all the fields
|
|
||||||
# takes section_id param
|
|
||||||
def get_fields(s_id):
|
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
|
# create dict of arrays for fields
|
||||||
field_set = {"fields": []}
|
field_set = {"fields": []}
|
||||||
queryset = Field.objects.filter(section_id=s_id).order_by('number')
|
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):
|
def generate_named_fields_for_section(fields):
|
||||||
'''
|
"""
|
||||||
Converts a section's field data into key-value pairs
|
Prepares a dictionary of key-value pairs based on the raw fields
|
||||||
for use in policy rule lambda functions.
|
passed in. Used to pass into the rule lambda functions.
|
||||||
'''
|
|
||||||
|
fields -- Python object prepared by get_fields
|
||||||
|
"""
|
||||||
result = {}
|
result = {}
|
||||||
for field in fields:
|
for field in fields:
|
||||||
key = field['field_name']
|
key = field['field_name']
|
||||||
|
@ -98,13 +109,16 @@ def generate_named_fields_for_section(fields):
|
||||||
result[key] = value
|
result[key] = value
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# API Endpoints
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
def report(request):
|
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
|
# Create the report
|
||||||
report = Report.objects.create(user_id=request.user, title=request.data['title'],
|
report = Report.objects.create(user_id=request.user, title=request.data['title'],
|
||||||
date_created=datetime.date.today())
|
date_created=datetime.date.today())
|
||||||
|
@ -127,12 +141,15 @@ def report(request):
|
||||||
f.save()
|
f.save()
|
||||||
|
|
||||||
# Return the newly created report
|
# Return the newly created report
|
||||||
data = get_reports(report.id)
|
data = get_report(report.id)
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|
||||||
# View the list of reports
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
def reports(request):
|
def reports(request):
|
||||||
|
"""
|
||||||
|
Returns a condensed version of the current user's reports in json
|
||||||
|
format.
|
||||||
|
"""
|
||||||
report_set = {"reports": []}
|
report_set = {"reports": []}
|
||||||
queryset = Report.objects.all().filter(user_id=request.user.id).order_by('date_created')
|
queryset = Report.objects.all().filter(user_id=request.user.id).order_by('date_created')
|
||||||
for i in queryset:
|
for i in queryset:
|
||||||
|
@ -150,31 +167,40 @@ def reports(request):
|
||||||
return JsonResponse(report_set)
|
return JsonResponse(report_set)
|
||||||
|
|
||||||
def user_owns_report(user, report):
|
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)
|
report_to_check = Report.objects.filter(id=report)
|
||||||
if len(report_to_check) < 1:
|
if len(report_to_check) < 1:
|
||||||
return False
|
return False
|
||||||
return report_to_check[0].user_id == user
|
return report_to_check[0].user_id == user
|
||||||
|
|
||||||
# actions for an individual report
|
|
||||||
@api_view(['GET', 'PUT', 'DELETE'])
|
@api_view(['GET', 'PUT', 'DELETE'])
|
||||||
def report_detail(request, report_pk):
|
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
|
# Check that the user owns the report
|
||||||
if not user_owns_report(user=request.user, report=report_pk):
|
if not user_owns_report(user=request.user, report=report_pk):
|
||||||
return JsonResponse({"message": "Current user does not own the specified report."}, status=401)
|
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':
|
if request.method == 'GET':
|
||||||
data = get_reports(report_pk)
|
data = get_report(report_pk)
|
||||||
return JsonResponse(data)
|
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':
|
elif request.method == 'PUT':
|
||||||
return JsonResponse({"message": "Report submitted."})
|
return JsonResponse({"message": "Report submitted."})
|
||||||
|
|
||||||
# Delete the report
|
# DELETE: Deletes a report from the user's account.
|
||||||
elif request.method == 'DELETE':
|
elif request.method == 'DELETE':
|
||||||
# get corresponding sections
|
# get corresponding sections
|
||||||
section_set = Section.objects.filter(report_id=report_pk)
|
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)})
|
return JsonResponse({"message": "Deleted report: {0}.".format(title)})
|
||||||
|
|
||||||
def user_owns_section(user, section):
|
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)
|
section_to_check = Section.objects.filter(id=section)
|
||||||
if len(section_to_check) < 1:
|
if len(section_to_check) < 1:
|
||||||
return False
|
return False
|
||||||
report_to_check = section_to_check[0].report_id
|
report_to_check = section_to_check[0].report_id
|
||||||
return report_to_check.user_id == user
|
return report_to_check.user_id == user
|
||||||
|
|
||||||
# update a section with new data
|
|
||||||
@api_view(['PUT'])
|
@api_view(['PUT'])
|
||||||
def section(request, report_pk, section_pk):
|
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
|
# Check that the user owns the report
|
||||||
if not user_owns_section(user=request.user, section=section_pk):
|
if not user_owns_section(user=request.user, section=section_pk):
|
||||||
return JsonResponse({"message": "Current user does not own the specified section."}, status=401)
|
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,
|
"completed": s.completed,
|
||||||
"title": s.title,
|
"title": s.title,
|
||||||
"html_description": s.html_description,
|
"html_description": s.html_description,
|
||||||
|
"rule_violations": [],
|
||||||
}
|
}
|
||||||
data.update(get_fields(s.id))
|
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)
|
return JsonResponse(data)
|
||||||
|
|
||||||
# function checks if a field is complete
|
|
||||||
def section_complete(section_pk):
|
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
|
# grab field set
|
||||||
check_fields = Field.objects.filter(section_id=section_pk)
|
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
|
from django.db import models
|
||||||
|
|
||||||
class CustomUser(AbstractUser):
|
class CustomUser(AbstractUser):
|
||||||
|
"""
|
||||||
|
Custom user model to allow for new fields if necessary.
|
||||||
|
"""
|
||||||
age = models.PositiveIntegerField(null=True, blank=True)
|
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 _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
class RegisterSerializer(serializers.Serializer):
|
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)
|
email = serializers.EmailField(required=allauth_settings.EMAIL_REQUIRED)
|
||||||
first_name = serializers.CharField(required=True, write_only=True)
|
first_name = serializers.CharField(required=True, write_only=True)
|
||||||
last_name = serializers.CharField(required=True, write_only=True)
|
last_name = serializers.CharField(required=True, write_only=True)
|
||||||
|
|
Loading…
Reference in a new issue