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…
	
	Add table
		
		Reference in a new issue
	
	 Preston Doman
						Preston Doman