From 12d1f9fd979c11b9e3a3a89b1595b07569b88f79 Mon Sep 17 00:00:00 2001 From: workmai Date: Thu, 4 Oct 2018 12:14:39 -0600 Subject: Initial commit of the coding assignment base project --- procurement/__init__.py | 0 procurement/admin.py | 31 ++++++++ procurement/admin_forms.py | 20 ++++++ procurement/api.py | 14 ++++ procurement/apps.py | 5 ++ procurement/forms.py | 14 ++++ procurement/migrations/0001_initial.py | 47 ++++++++++++ procurement/migrations/__init__.py | 0 procurement/models.py | 83 ++++++++++++++++++++++ procurement/serializers.py | 17 +++++ procurement/static/procurement/css/suppliers.css | 13 ++++ .../static/procurement/js/component_search.js | 3 + .../admin_templates/source_components.html | 45 ++++++++++++ .../templates/procurement/documentation.html | 76 ++++++++++++++++++++ .../procurement/includes/supplier_list.html | 37 ++++++++++ .../templates/procurement/source_components.html | 61 ++++++++++++++++ procurement/urls.py | 12 ++++ procurement/views.py | 54 ++++++++++++++ 18 files changed, 532 insertions(+) create mode 100644 procurement/__init__.py create mode 100644 procurement/admin.py create mode 100644 procurement/admin_forms.py create mode 100644 procurement/api.py create mode 100644 procurement/apps.py create mode 100644 procurement/forms.py create mode 100644 procurement/migrations/0001_initial.py create mode 100644 procurement/migrations/__init__.py create mode 100644 procurement/models.py create mode 100644 procurement/serializers.py create mode 100644 procurement/static/procurement/css/suppliers.css create mode 100644 procurement/static/procurement/js/component_search.js create mode 100644 procurement/templates/procurement/admin_templates/source_components.html create mode 100644 procurement/templates/procurement/documentation.html create mode 100644 procurement/templates/procurement/includes/supplier_list.html create mode 100644 procurement/templates/procurement/source_components.html create mode 100644 procurement/urls.py create mode 100644 procurement/views.py (limited to 'procurement') diff --git a/procurement/__init__.py b/procurement/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/procurement/admin.py b/procurement/admin.py new file mode 100644 index 0000000..993b1b5 --- /dev/null +++ b/procurement/admin.py @@ -0,0 +1,31 @@ +from django.contrib import admin +from django.shortcuts import render_to_response, get_object_or_404 + +from procurement.admin_forms import ComponentAdminForm +from procurement.models import Supplier, Component + + +class SupplierAdmin(admin.ModelAdmin): + list_display = ('name', 'representative_name', 'representative_email', 'is_authorized', 'updated') + filter_horizonal = ('components',) + + +class ComponentAdmin(admin.ModelAdmin): + list_display = ('name', 'sku', 'updated') + form = ComponentAdminForm + source_components_template = 'procurement/admin_templates/source_components.html' + + def source_components(self, request, pk): + component = get_object_or_404(Component, pk=pk) + + return render_to_response(self.source_components_template, { + 'title': 'Source Suppliers for: %s' % component, + 'opts': self.model._meta, + 'component': component, + 'supplier_results': component.suppliers.filter(is_authorized=True), + + }) + + +admin.site.register(Supplier, SupplierAdmin) +admin.site.register(Component, ComponentAdmin) diff --git a/procurement/admin_forms.py b/procurement/admin_forms.py new file mode 100644 index 0000000..b7bbd1c --- /dev/null +++ b/procurement/admin_forms.py @@ -0,0 +1,20 @@ +from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple + +from procurement.models import Supplier, Component + + +class ComponentAdminForm(forms.ModelForm): + suppliers = forms.ModelMultipleChoiceField( + queryset=Supplier.objects.filter(is_authorized=True), + required=False, + widget=FilteredSelectMultiple( + verbose_name='Suppliers', + is_stacked=False + ) + + ) + + class Meta: + model = Component + fields = ['name', 'sku', 'suppliers'] \ No newline at end of file diff --git a/procurement/api.py b/procurement/api.py new file mode 100644 index 0000000..0419ce8 --- /dev/null +++ b/procurement/api.py @@ -0,0 +1,14 @@ +from rest_framework.generics import ListAPIView, RetrieveAPIView + +from procurement.models import Component +from procurement.serializers import ComponentSerializer + + +class ComponentAPIList(ListAPIView): + queryset = Component.objects.all() + serializer_class = ComponentSerializer + + +class ComponentAPIRetrieve(RetrieveAPIView): + queryset = Component.objects.all() + serializer_class = ComponentSerializer diff --git a/procurement/apps.py b/procurement/apps.py new file mode 100644 index 0000000..94c7e8e --- /dev/null +++ b/procurement/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProcurementConfig(AppConfig): + name = 'procurement' diff --git a/procurement/forms.py b/procurement/forms.py new file mode 100644 index 0000000..4e8b0aa --- /dev/null +++ b/procurement/forms.py @@ -0,0 +1,14 @@ +from django import forms +from procurement.models import Component + + +class ComponentSearchForm(forms.Form): + component = forms.ModelChoiceField( + queryset=Component.objects.all(), + required=False + ) + + def __init__(self, *args, **kwargs): + super(ComponentSearchForm, self).__init__(*args, **kwargs) + self.fields['component'].widget.attrs.update({"class": "form-control"}) + diff --git a/procurement/migrations/0001_initial.py b/procurement/migrations/0001_initial.py new file mode 100644 index 0000000..ffabc0e --- /dev/null +++ b/procurement/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 2.1.2 on 2018-10-02 14:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Component', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255)), + ('sku', models.CharField(max_length=50)), + ], + options={ + 'ordering': ('name',), + }, + ), + migrations.CreateModel( + name='Supplier', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255)), + ('representative_name', models.CharField(blank=True, max_length=255, null=True)), + ('representative_email', models.EmailField(blank=True, max_length=255, null=True)), + ('is_authorized', models.BooleanField()), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='component', + name='suppliers', + field=models.ManyToManyField(blank=True, related_name='components', to='procurement.Supplier'), + ), + ] diff --git a/procurement/migrations/__init__.py b/procurement/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/procurement/models.py b/procurement/models.py new file mode 100644 index 0000000..1e70736 --- /dev/null +++ b/procurement/models.py @@ -0,0 +1,83 @@ +from django.db import models +from django.utils.timezone import now as timezone_now + +MONTH = 30 * 24 * 60 * 60 +WEEK = 7 * 24 * 60 * 60 +DAY = 24 * 60 * 60 +HOUR = 60 * 60 +MINUTE = 60 + + +class DashboardModel(models.Model): + """ + Abstract base model for things which will be displayed on the dashboard, adds in created and updated fields, + and provides a convenience method which provides a nicely formatted string of the time since update. + """ + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + @property + def time_since_update(self): + update_delta = timezone_now() - self.updated + seconds_since_update = update_delta.seconds + + if seconds_since_update / MONTH >= 1: + quantity = seconds_since_update / MONTH + units = 'months' if quantity > 1 else 'month' + + elif seconds_since_update / WEEK >= 1: + quantity = seconds_since_update / WEEK + units = 'weeks' if quantity > 1 else 'week' + + elif seconds_since_update / DAY >= 1: + quantity = seconds_since_update / DAY + units = 'days' if quantity > 1 else 'day' + + elif seconds_since_update / HOUR >= 1: + quantity = seconds_since_update / HOUR + units = 'hours' if quantity > 1 else 'hour' + + elif seconds_since_update / MINUTE >= 1: + quantity = seconds_since_update / MINUTE + units = 'minutes' if quantity > 1 else 'minute' + + else: + return "updated just now" + + # Ensure the quantity output is rounded to 2 decimal places + base_string = 'updated {quantity:.2f} {units} ago' + return base_string.format(quantity=quantity, units=units) + + +class Supplier(DashboardModel): + """ + Model which represents an individual or organisation which supplies components + """ + name = models.CharField(max_length=255) + representative_name = models.CharField(max_length=255, null=True, blank=True) + representative_email = models.EmailField(max_length=255, null=True, blank=True) + is_authorized = models.BooleanField() + + def __str__(self): + return '{}'.format(self.name) + + +class Component(DashboardModel): + """ + Model which represents items which may be supplied. + """ + name = models.CharField(max_length=255) + sku = models.CharField(max_length=50) + suppliers = models.ManyToManyField(Supplier, related_name='components', blank=True) + + class Meta: + ordering = ("name",) + + def __str__(self): + return '{} ({})'.format( + self.name, + self.sku + ) diff --git a/procurement/serializers.py b/procurement/serializers.py new file mode 100644 index 0000000..1840837 --- /dev/null +++ b/procurement/serializers.py @@ -0,0 +1,17 @@ +from rest_framework import serializers +from procurement.models import Component, Supplier + + +class SupplierSerializer(serializers.ModelSerializer): + class Meta: + model = Supplier + exclude = ('created', 'updated') + + +class ComponentSerializer(serializers.ModelSerializer): + text = serializers.CharField(source='__str__', read_only=True) + suppliers = SupplierSerializer(many=True, read_only=True) + + class Meta: + model = Component + exclude = ('created', 'updated') diff --git a/procurement/static/procurement/css/suppliers.css b/procurement/static/procurement/css/suppliers.css new file mode 100644 index 0000000..396a446 --- /dev/null +++ b/procurement/static/procurement/css/suppliers.css @@ -0,0 +1,13 @@ +/* hide component field on search form */ +#id_component { + display: none; +} +label[for=id_component] { + display: none; +} +.record-list { + display: block; +} +.record-updated{ + display: block; +} \ No newline at end of file diff --git a/procurement/static/procurement/js/component_search.js b/procurement/static/procurement/js/component_search.js new file mode 100644 index 0000000..cba0a16 --- /dev/null +++ b/procurement/static/procurement/js/component_search.js @@ -0,0 +1,3 @@ +$(document).ready(function(){ + $("#id_component").select2(); +}); \ No newline at end of file diff --git a/procurement/templates/procurement/admin_templates/source_components.html b/procurement/templates/procurement/admin_templates/source_components.html new file mode 100644 index 0000000..2deae1e --- /dev/null +++ b/procurement/templates/procurement/admin_templates/source_components.html @@ -0,0 +1,45 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_modify %} +{% block extrahead %}{{ block.super }} + + + + + {% for stylesheet in module_stylesheets %} + + {% endfor %} +{% url 'admin:jsi18n' as jsi18nurl %} +{% endblock %} +{% block extrastyle %}{{ block.super }} +{% endblock %} +{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} +{% block breadcrumbs %}{% if not is_popup %} + +{% endif %}{% endblock %} + +{% block content %} +
+ + + {% if supplier_results != None %} + {% include "suppliers/includes/supplier_list.html" %} + {% endif %} +
+ + + + + + + + + + + + {% for script in module_javascripts %} + + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/procurement/templates/procurement/documentation.html b/procurement/templates/procurement/documentation.html new file mode 100644 index 0000000..4775890 --- /dev/null +++ b/procurement/templates/procurement/documentation.html @@ -0,0 +1,76 @@ +{% extends 'base.html' %} + +{% block main_content_area %} +
+ + +
+ +
+
+
+

Introduction

+

+ This is a very simple component and supplier management system. The intention function of the system + is to maintain a list of components, and the suppliers who are authorised to supply them. Administrative + staff maintain the list of suppliers, components and their associations, while general staff simply + use the from page to look up a component, and see its list of suppliers. +

+

+ The system is written in Python (v3.6), using the Django framework (v2.1.2). Documentation for Django + is available here. +

+ + +

Adding Data

+

+ You can manage the components and suppliers using the + django admin panel. +

+ +

Supplier List

+

+ Components and their approved Suppliers can be located under + source components. From this page you may search for a + component, and then click the Find Suppliers button to display its Suppliers +

+ +

API

+

+ A simple REST api is integrated into the system, using the Django Rest Framework. + It provides a means of querying all components or, by providing an id + in the url, a specific component. +

+
+ +
+ +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/procurement/templates/procurement/includes/supplier_list.html b/procurement/templates/procurement/includes/supplier_list.html new file mode 100644 index 0000000..028e62c --- /dev/null +++ b/procurement/templates/procurement/includes/supplier_list.html @@ -0,0 +1,37 @@ + + +
+
+ Approved Suppliers for {{ component }} +
+ +
+
+ + + + + + + + + + {% for supplier in supplier_results %} + + + + + + {% empty %} + + + + {% endfor %} + +
CompanyRepresentativeEmail
{{ supplier.name }}{{ supplier.representative_name }}{{ supplier.representative_email }}
No authorized suppliers found.
+
+ +
+ +
+ \ No newline at end of file diff --git a/procurement/templates/procurement/source_components.html b/procurement/templates/procurement/source_components.html new file mode 100644 index 0000000..3eebf84 --- /dev/null +++ b/procurement/templates/procurement/source_components.html @@ -0,0 +1,61 @@ +{% extends 'base.html' %} +{% load static %} + +{% block additional_css %} + + +{% endblock %} + +{% block main_content_area %} +
+
+
+
+
+ {% csrf_token %} + {{ form }} +
+
+ +
+
+
+ +
+ + {% if supplier_results != None %} + {% include 'procurement/includes/supplier_list.html' %} + {% endif %} +
+ + +
+
+
+ Searching Procurement Records +
+ +
+
+ + {{ supplier_count }} Suppliers + {{ suppliers_last_updated }} + + + {{ component_count }} Components + {{ components_last_updated }} + +
+ +
+ +
+ +
+ +{% endblock %} + +{% block additional_js %} + + +{% endblock %} \ No newline at end of file diff --git a/procurement/urls.py b/procurement/urls.py new file mode 100644 index 0000000..d629635 --- /dev/null +++ b/procurement/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from procurement.views import ComponentSearchView, DocumentationView +from procurement.api import ComponentAPIList, ComponentAPIRetrieve + +urlpatterns = [ + path('', ComponentSearchView.as_view(), name='component-search'), + path('documentation', DocumentationView.as_view(), name='documentation'), + + # API Urls + path('api/components/', ComponentAPIList.as_view(), name='api-component-list'), + path('api/components//', ComponentAPIRetrieve.as_view(), name='api-component-retrieve'), +] diff --git a/procurement/views.py b/procurement/views.py new file mode 100644 index 0000000..8891f89 --- /dev/null +++ b/procurement/views.py @@ -0,0 +1,54 @@ +from django.views.generic import FormView, TemplateView + +from procurement.forms import ComponentSearchForm +from procurement.models import Supplier, Component + + +class ComponentSearchView(FormView): + template_name = 'procurement/source_components.html' + form_class = ComponentSearchForm + + component = None + supplier_results = None + + def get_context_data(self): + context = super().get_context_data() + + try: + suppliers_last_updated = Supplier.objects.latest('updated').time_since_update + except Supplier.DoesNotExist: + suppliers_last_updated = '' + + try: + components_last_updated = Component.objects.latest('updated').time_since_update + except Component.DoesNotExist: + components_last_updated = '' + + context.update({ + 'page_name': 'Component Search', + 'component': self.component, + 'supplier_results': self.supplier_results, + 'supplier_count': Supplier.objects.all().count(), + 'suppliers_last_updated': suppliers_last_updated, + 'component_count': Component.objects.all().count(), + 'components_last_updated': components_last_updated, + }) + return context + + def form_valid(self, form): + self.component = form.cleaned_data['component'] + if self.component: + self.supplier_results = self.component.suppliers.filter(is_authorized=True) + + return super(ComponentSearchView, self).get(self.request) + + +class DocumentationView(TemplateView): + template_name = 'procurement/documentation.html' + + def get_context_data(self): + context = super().get_context_data() + context.update({ + 'page_name': 'Documentation', + }) + return context -- cgit v1.1