summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--README.md1
-rw-r--r--coding_assignment_project/__init__.py0
-rw-r--r--coding_assignment_project/settings.py125
-rw-r--r--coding_assignment_project/static/css/lightsource-styles.css16
-rw-r--r--coding_assignment_project/static/css/sb-admin-2.css354
-rw-r--r--coding_assignment_project/static/js/sb-admin-2.js31
-rw-r--r--coding_assignment_project/templates/__init__.py0
-rw-r--r--coding_assignment_project/templates/base.html114
-rw-r--r--coding_assignment_project/urls.py7
-rw-r--r--coding_assignment_project/wsgi.py16
-rwxr-xr-xmanage.py15
-rw-r--r--procurement/__init__.py0
-rw-r--r--procurement/admin.py31
-rw-r--r--procurement/admin_forms.py20
-rw-r--r--procurement/api.py14
-rw-r--r--procurement/apps.py5
-rw-r--r--procurement/forms.py14
-rw-r--r--procurement/migrations/0001_initial.py47
-rw-r--r--procurement/migrations/__init__.py0
-rw-r--r--procurement/models.py83
-rw-r--r--procurement/serializers.py17
-rw-r--r--procurement/static/procurement/css/suppliers.css13
-rw-r--r--procurement/static/procurement/js/component_search.js3
-rw-r--r--procurement/templates/procurement/admin_templates/source_components.html45
-rw-r--r--procurement/templates/procurement/documentation.html76
-rw-r--r--procurement/templates/procurement/includes/supplier_list.html37
-rw-r--r--procurement/templates/procurement/source_components.html61
-rw-r--r--procurement/urls.py12
-rw-r--r--procurement/views.py54
-rw-r--r--requirements.txt2
31 files changed, 1215 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..47361ff
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.idea/
+.python-version \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3f857b4
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# Coding Assignment Project
diff --git a/coding_assignment_project/__init__.py b/coding_assignment_project/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/coding_assignment_project/__init__.py
diff --git a/coding_assignment_project/settings.py b/coding_assignment_project/settings.py
new file mode 100644
index 0000000..9afeb50
--- /dev/null
+++ b/coding_assignment_project/settings.py
@@ -0,0 +1,125 @@
+"""
+Django settings for coding_assignment_project project.
+
+Generated by 'django-admin startproject' using Django 2.1.2.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/2.1/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '6e+_000iv_ee1gv)^ls6%%x(=veqnx^k!))0t12x!^p&mou@1i'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'rest_framework',
+ 'procurement.apps.ProcurementConfig',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'coding_assignment_project.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': ['coding_assignment_project/templates'],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'coding_assignment_project.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/2.1/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'Canada/Saskatchewan'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/2.1/howto/static-files/
+
+STATIC_URL = '/static/'
+STATICFILES_DIRS = [
+ os.path.join(BASE_DIR, 'coding_assignment_project', 'static')
+]
diff --git a/coding_assignment_project/static/css/lightsource-styles.css b/coding_assignment_project/static/css/lightsource-styles.css
new file mode 100644
index 0000000..7ada411
--- /dev/null
+++ b/coding_assignment_project/static/css/lightsource-styles.css
@@ -0,0 +1,16 @@
+.lightsource-header {
+ background-color: #008080;
+}
+.navbar-default .navbar-brand,
+.navbar-default .navbar-brand:focus,
+.navbar-default .navbar-brand:hover {
+ color: #fff;
+}
+.panel-default>.panel-heading {
+ background-color: #008080;
+ color: #fff;
+}
+
+.btn-primary {
+ background-color: #008080;
+} \ No newline at end of file
diff --git a/coding_assignment_project/static/css/sb-admin-2.css b/coding_assignment_project/static/css/sb-admin-2.css
new file mode 100644
index 0000000..e8be396
--- /dev/null
+++ b/coding_assignment_project/static/css/sb-admin-2.css
@@ -0,0 +1,354 @@
+/*!
+ * Start Bootstrap - SB Admin 2 Bootstrap Admin Theme (http://startbootstrap.com)
+ * Code licensed under the Apache License v2.0.
+ * For details, see http://www.apache.org/licenses/LICENSE-2.0.
+ */
+
+body {
+ background-color: #f8f8f8;
+}
+
+#wrapper {
+ width: 100%;
+}
+
+#page-wrapper {
+ padding: 0 15px;
+ min-height: 568px;
+ background-color: #fff;
+}
+
+@media(min-width:768px) {
+ #page-wrapper {
+ position: inherit;
+ margin: 0 0 0 250px;
+ padding: 0 30px;
+ border-left: 1px solid #e7e7e7;
+ }
+}
+
+.navbar-top-links {
+ margin-right: 0;
+}
+
+.navbar-top-links li {
+ display: inline-block;
+}
+
+.navbar-top-links li:last-child {
+ margin-right: 15px;
+}
+
+.navbar-top-links li a {
+ padding: 15px;
+ min-height: 50px;
+}
+
+.navbar-top-links .dropdown-menu li {
+ display: block;
+}
+
+.navbar-top-links .dropdown-menu li:last-child {
+ margin-right: 0;
+}
+
+.navbar-top-links .dropdown-menu li a {
+ padding: 3px 20px;
+ min-height: 0;
+}
+
+.navbar-top-links .dropdown-menu li a div {
+ white-space: normal;
+}
+
+.navbar-top-links .dropdown-messages,
+.navbar-top-links .dropdown-tasks,
+.navbar-top-links .dropdown-alerts {
+ width: 310px;
+ min-width: 0;
+}
+
+.navbar-top-links .dropdown-messages {
+ margin-left: 5px;
+}
+
+.navbar-top-links .dropdown-tasks {
+ margin-left: -59px;
+}
+
+.navbar-top-links .dropdown-alerts {
+ margin-left: -123px;
+}
+
+.navbar-top-links .dropdown-user {
+ right: 0;
+ left: auto;
+}
+
+.sidebar .sidebar-nav.navbar-collapse {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+.sidebar .sidebar-search {
+ padding: 15px;
+}
+
+.sidebar ul li {
+ border-bottom: 1px solid #e7e7e7;
+}
+
+.sidebar ul li a.active {
+ background-color: #eee;
+}
+
+.sidebar .arrow {
+ float: right;
+}
+
+.sidebar .fa.arrow:before {
+ content: "\f104";
+}
+
+.sidebar .active>a>.fa.arrow:before {
+ content: "\f107";
+}
+
+.sidebar .nav-second-level li,
+.sidebar .nav-third-level li {
+ border-bottom: 0!important;
+}
+
+.sidebar .nav-second-level li a {
+ padding-left: 37px;
+}
+
+.sidebar .nav-third-level li a {
+ padding-left: 52px;
+}
+
+@media(min-width:768px) {
+ .sidebar {
+ z-index: 1;
+ position: absolute;
+ width: 250px;
+ margin-top: 51px;
+ }
+
+ .navbar-top-links .dropdown-messages,
+ .navbar-top-links .dropdown-tasks,
+ .navbar-top-links .dropdown-alerts {
+ margin-left: auto;
+ }
+}
+
+.btn-outline {
+ color: inherit;
+ background-color: transparent;
+ transition: all .5s;
+}
+
+.btn-primary.btn-outline {
+ color: #428bca;
+}
+
+.btn-success.btn-outline {
+ color: #5cb85c;
+}
+
+.btn-info.btn-outline {
+ color: #5bc0de;
+}
+
+.btn-warning.btn-outline {
+ color: #f0ad4e;
+}
+
+.btn-danger.btn-outline {
+ color: #d9534f;
+}
+
+.btn-primary.btn-outline:hover,
+.btn-success.btn-outline:hover,
+.btn-info.btn-outline:hover,
+.btn-warning.btn-outline:hover,
+.btn-danger.btn-outline:hover {
+ color: #fff;
+}
+
+.chat {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.chat li {
+ margin-bottom: 10px;
+ padding-bottom: 5px;
+ border-bottom: 1px dotted #999;
+}
+
+.chat li.left .chat-body {
+ margin-left: 60px;
+}
+
+.chat li.right .chat-body {
+ margin-right: 60px;
+}
+
+.chat li .chat-body p {
+ margin: 0;
+}
+
+.panel .slidedown .glyphicon,
+.chat .glyphicon {
+ margin-right: 5px;
+}
+
+.chat-panel .panel-body {
+ height: 350px;
+ overflow-y: scroll;
+}
+
+.login-panel {
+ margin-top: 25%;
+}
+
+.flot-chart {
+ display: block;
+ height: 400px;
+}
+
+.flot-chart-content {
+ width: 100%;
+ height: 100%;
+}
+
+.dataTables_wrapper {
+ position: relative;
+ clear: both;
+}
+
+table.dataTable thead .sorting,
+table.dataTable thead .sorting_asc,
+table.dataTable thead .sorting_desc,
+table.dataTable thead .sorting_asc_disabled,
+table.dataTable thead .sorting_desc_disabled {
+ background: 0 0;
+}
+
+table.dataTable thead .sorting_asc:after {
+ content: "\f0de";
+ float: right;
+ font-family: fontawesome;
+}
+
+table.dataTable thead .sorting_desc:after {
+ content: "\f0dd";
+ float: right;
+ font-family: fontawesome;
+}
+
+table.dataTable thead .sorting:after {
+ content: "\f0dc";
+ float: right;
+ font-family: fontawesome;
+ color: rgba(50,50,50,.5);
+}
+
+.btn-circle {
+ width: 30px;
+ height: 30px;
+ padding: 6px 0;
+ border-radius: 15px;
+ text-align: center;
+ font-size: 12px;
+ line-height: 1.428571429;
+}
+
+.btn-circle.btn-lg {
+ width: 50px;
+ height: 50px;
+ padding: 10px 16px;
+ border-radius: 25px;
+ font-size: 18px;
+ line-height: 1.33;
+}
+
+.btn-circle.btn-xl {
+ width: 70px;
+ height: 70px;
+ padding: 10px 16px;
+ border-radius: 35px;
+ font-size: 24px;
+ line-height: 1.33;
+}
+
+.show-grid [class^=col-] {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ border: 1px solid #ddd;
+ background-color: #eee!important;
+}
+
+.show-grid {
+ margin: 15px 0;
+}
+
+.huge {
+ font-size: 40px;
+}
+
+.panel-green {
+ border-color: #5cb85c;
+}
+
+.panel-green .panel-heading {
+ border-color: #5cb85c;
+ color: #fff;
+ background-color: #5cb85c;
+}
+
+.panel-green a {
+ color: #5cb85c;
+}
+
+.panel-green a:hover {
+ color: #3d8b3d;
+}
+
+.panel-red {
+ border-color: #d9534f;
+}
+
+.panel-red .panel-heading {
+ border-color: #d9534f;
+ color: #fff;
+ background-color: #d9534f;
+}
+
+.panel-red a {
+ color: #d9534f;
+}
+
+.panel-red a:hover {
+ color: #b52b27;
+}
+
+.panel-yellow {
+ border-color: #f0ad4e;
+}
+
+.panel-yellow .panel-heading {
+ border-color: #f0ad4e;
+ color: #fff;
+ background-color: #f0ad4e;
+}
+
+.panel-yellow a {
+ color: #f0ad4e;
+}
+
+.panel-yellow a:hover {
+ color: #df8a13;
+} \ No newline at end of file
diff --git a/coding_assignment_project/static/js/sb-admin-2.js b/coding_assignment_project/static/js/sb-admin-2.js
new file mode 100644
index 0000000..96bc576
--- /dev/null
+++ b/coding_assignment_project/static/js/sb-admin-2.js
@@ -0,0 +1,31 @@
+
+//Loads the correct sidebar on window load,
+//collapses the sidebar on window resize.
+// Sets the min-height of #page-wrapper to window size
+$(function() {
+ $(window).bind("load resize", function() {
+ topOffset = 50;
+ width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width;
+ if (width < 768) {
+ $('div.navbar-collapse').addClass('collapse');
+ topOffset = 100; // 2-row-menu
+ } else {
+ $('div.navbar-collapse').removeClass('collapse');
+ }
+
+ height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1;
+ height = height - topOffset;
+ if (height < 1) height = 1;
+ if (height > topOffset) {
+ $("#page-wrapper").css("min-height", (height) + "px");
+ }
+ });
+
+ var url = window.location;
+ var element = $('ul.nav a').filter(function() {
+ return this.href == url || url.href.indexOf(this.href) == 0;
+ }).addClass('active').parent().parent().addClass('in').parent();
+ if (element.is('li')) {
+ element.addClass('active');
+ }
+});
diff --git a/coding_assignment_project/templates/__init__.py b/coding_assignment_project/templates/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/coding_assignment_project/templates/__init__.py
diff --git a/coding_assignment_project/templates/base.html b/coding_assignment_project/templates/base.html
new file mode 100644
index 0000000..231c385
--- /dev/null
+++ b/coding_assignment_project/templates/base.html
@@ -0,0 +1,114 @@
+{% load static %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="description" content="">
+ <meta name="author" content="">
+
+ <title>Procurement Dashboard</title>
+
+ <!-- Bootstrap Core CSS -->
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+ <!-- Custom CSS -->
+ <link href="{% static 'css/sb-admin-2.css' %}" rel="stylesheet">
+
+ <!-- Font Awesome -->
+ <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">
+
+ <!-- Lightsource Custom Styles -->
+ <link href="{% static 'css/lightsource-styles.css' %}" rel="stylesheet" type="text/css">
+
+ {% block additional_css %}
+ {% endblock %}
+</head>
+
+<body>
+
+ <div id="wrapper">
+
+ <!-- Navigation -->
+ <nav class="navbar navbar-default navbar-static-top lightsource-header" role="navigation" style="margin-bottom: 0">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand" href="{% url 'component-search' %}">Light Source</a>
+ </div>
+ <!-- /.navbar-header -->
+
+
+ <div class="navbar-default sidebar" role="navigation">
+ <div class="sidebar-nav navbar-collapse">
+ <ul class="nav" id="side-menu">
+ <li class="sidebar-search">
+ <div class="input-group">
+ <h4><small></small></h4>
+ </div>
+ <!-- /input-group -->
+ </li>
+ <li>
+ <a href="{% url 'documentation' %}" {% if page_name == "documentation" %} class="active"{% endif %}><i class="fa fa-info-circle fa-fw"></i> Documentation</a>
+ </li>
+ <li>
+ <a href="{% url 'component-search' %}" {% if page_name == "source components" %} class="active"{% endif %}><i class="fa fa-th-list fa-fw"></i> Source Components</a>
+ </li>
+
+ </ul>
+ </div>
+ <!-- /.sidebar-collapse -->
+ </div>
+ <!-- /.navbar-static-side -->
+ </nav>
+
+ <div id="page-wrapper">
+ <div class="row" id="title-bar">
+ <div class="col-lg-12">
+ <h1 class="page-header">{{ page_name | title }}</h1>
+ </div>
+ <!-- /.col-lg-12 -->
+ </div>
+
+ <!-- /.row -->
+ <div class="row">
+ {% block main_content_area %}
+ {% endblock %}
+ </div>
+ <!-- /.row -->
+ </div>
+ <!-- /#page-wrapper -->
+
+ </div>
+ <!-- /#wrapper -->
+
+ <!-- jQuery -->
+ <script
+ src="https://code.jquery.com/jquery-3.3.1.min.js"
+ integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
+ crossorigin="anonymous"></script>
+
+ <script
+ src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"
+ integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU="
+ crossorigin="anonymous"></script>
+
+ <!-- Bootstrap Core JavaScript -->
+ <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
+
+ <!-- Custom Theme JavaScript -->
+ <script src="{% static 'js/sb-admin-2.js' %}"></script>
+
+ {% block additional_js %}
+ {% endblock %}
+
+</body>
+
+</html>
diff --git a/coding_assignment_project/urls.py b/coding_assignment_project/urls.py
new file mode 100644
index 0000000..7fcafc7
--- /dev/null
+++ b/coding_assignment_project/urls.py
@@ -0,0 +1,7 @@
+from django.contrib import admin
+from django.urls import path, include
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+ path('', include('procurement.urls'))
+]
diff --git a/coding_assignment_project/wsgi.py b/coding_assignment_project/wsgi.py
new file mode 100644
index 0000000..3057f8d
--- /dev/null
+++ b/coding_assignment_project/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for coding_assignment_project project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'coding_assignment_project.settings')
+
+application = get_wsgi_application()
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..a171f51
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == '__main__':
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'coding_assignment_project.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
diff --git a/procurement/__init__.py b/procurement/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/procurement/__init__.py
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
--- /dev/null
+++ b/procurement/migrations/__init__.py
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 }}
+ <!-- Bootstrap Core CSS -->
+ <link href="/static/prebuilt/bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
+ <!-- Lightsource Custom Styles -->
+ <link href="/static/custom/css/lightsource-styles.css" rel="stylesheet" type="text/css">
+ {% for stylesheet in module_stylesheets %}
+ <link href="/static/{{ stylesheet }}" rel="stylesheet" type="text/css">
+ {% 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 %}
+<div class="breadcrumbs">
+ <a href="../../">{% trans "Home" %}</a> ›
+ <a href="../">{{ opts.app_label|capfirst|escape }}</a> ›
+ {% trans 'Source Components' %}</div>
+{% endif %}{% endblock %}
+
+{% block content %}
+ <div class="col-lg-8">
+
+ <!-- /.panel -->
+ {% if supplier_results != None %}
+ {% include "suppliers/includes/supplier_list.html" %}
+ {% endif %}
+ </div>
+
+ <!-- jQuery -->
+ <script src="/static/prebuilt/bower_components/jquery/dist/jquery.min.js"></script>
+ <script src="//code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
+ <link rel="stylesheet" href="//code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css">
+
+ <!-- Bootstrap Core JavaScript -->
+ <script src="/static/prebuilt/bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
+
+ <!-- Custom Theme JavaScript -->
+ <script src="/static/prebuilt/dist/js/sb-admin-2.js"></script>
+ {% for script in module_javascripts %}
+ <script src="/static/{{ script }}"></script>
+ {% 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 %}
+ <div class="col-lg-4">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Documentation
+ </div>
+ <!-- /.panel-heading -->
+ <div class="panel-body">
+ <div class="list-group">
+ <a href="#introduction" class="list-group-item">
+ <i class="fa fa-info-circle fa-fw"></i> Introduction
+ </a>
+ <a href="#adding-data" class="list-group-item">
+ <i class="fa fa-database fa-fw"></i> Adding Data
+ </a>
+ <a href="#supplier-list" class="list-group-item">
+ <i class="fa fa-th-list fa-fw"></i> Supplier List
+ </a>
+ <a href="#api" class="list-group-item">
+ <i class="fa fa-bolt fa-fw"></i> API
+ </a>
+ </div>
+ <!-- /.list-group -->
+ </div>
+ <!-- /.panel-body -->
+ </div>
+ <!-- /.panel -->
+ </div>
+
+ <div class="col-lg-8">
+ <div class="panel panel-default">
+ <div class="panel-body">
+ <h2><a name="introduction"></a>Introduction</h2>
+ <p>
+ 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.
+ </p>
+ <p>
+ The system is written in Python (v3.6), using the Django framework (v2.1.2). Documentation for Django
+ is available <a href="https://www.djangoproject.com/">here</a>.
+ </p>
+
+
+ <h2><a name="adding-data"></a>Adding Data</h2>
+ <p>
+ You can manage the components and suppliers using the
+ <a href="{% url 'admin:index' %}">django admin panel</a>.
+ </p>
+
+ <h2><a name="supplier-list"></a>Supplier List</h2>
+ <p>
+ Components and their approved Suppliers can be located under
+ <a href="{% url 'component-search' %}">source components</a>. From this page you may search for a
+ component, and then click the Find Suppliers button to display its Suppliers
+ </p>
+
+ <h2><a name="api"></a>API</h2>
+ <p>
+ A simple REST api is integrated into the system, using the <a href="https://www.django-rest-framework.org/">Django Rest Framework</a>.
+ It provides a means of querying <a href="{% url 'api-component-list' %}">all components</a> or, by providing an id
+ in the url, <a href="{% url 'api-component-retrieve' pk=1 %}">a specific component</a>.
+ </p>
+ </div>
+ <!-- /.panel-body -->
+ </div>
+ <!-- /.panel -->
+ </div>
+
+
+
+
+{% 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 @@
+
+<!-- Supplier Results -->
+<div class="panel panel-default">
+ <div class="panel-heading">
+ Approved Suppliers for {{ component }}
+ </div>
+ <!-- /.panel-heading -->
+ <div class="panel-body">
+ <div class="table-responsive">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>Company</th>
+ <th>Representative</th>
+ <th>Email</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for supplier in supplier_results %}
+ <tr>
+ <td>{{ supplier.name }}</td>
+ <td>{{ supplier.representative_name }}</td>
+ <td>{{ supplier.representative_email }}</td>
+ </tr>
+ {% empty %}
+ <tr>
+ <td colspan="3">No authorized suppliers found.</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ <!-- /.table-responsive -->
+ </div>
+ <!-- /.panel-body -->
+</div>
+<!-- / Supplier Results Panel --> \ 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 %}
+ <link href="{% static 'procurement/css/suppliers.css' %}" rel="stylesheet" type="text/css">
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css" rel="stylesheet" />
+{% endblock %}
+
+{% block main_content_area %}
+ <div class="col-lg-8">
+ <div class="panel panel-default">
+ <div class="panel-body">
+ <form id="find-suppliers" method="post">
+ <div class="form-group">
+ {% csrf_token %}
+ {{ form }}
+ </div>
+ <div class="form-group">
+ <input type="submit" value="Find Suppliers" id="find-suppliers-button" class="btn btn-primary">
+ </div>
+ </form>
+ </div>
+ <!-- /.panel-body -->
+ </div>
+ <!-- /.panel -->
+ {% if supplier_results != None %}
+ {% include 'procurement/includes/supplier_list.html' %}
+ {% endif %}
+ </div>
+
+
+ <div class="col-lg-4">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Searching Procurement Records
+ </div>
+ <!-- /.panel-heading -->
+ <div class="panel-body">
+ <div class="list-group">
+ <span class="list-group-item">
+ <span class="record-list"><i class="fa fa-parachute-box fa-fw"></i> {{ supplier_count }} Suppliers</span>
+ <span class="record-updated text-muted small"><em>{{ suppliers_last_updated }}</em></span>
+ </span>
+ <span class="list-group-item">
+ <span class="record-list"><i class="fa fa-cog fa-fw"></i> {{ component_count }} Components</span>
+ <span class="record-updated text-muted small"><em>{{ components_last_updated }}</em></span>
+ </span>
+ </div>
+ <!-- /.list-group -->
+ </div>
+ <!-- /.panel-body -->
+ </div>
+ <!-- /.panel -->
+ </div>
+
+{% endblock %}
+
+{% block additional_js %}
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/js/select2.min.js"></script>
+ <script src="{% static 'procurement/js/component_search.js' %}"></script>
+{% 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/<int:pk>/', 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
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4ea5aaf
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+Django==2.1.2
+djangorestframework==3.8.2