summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKyle McFarland <tfkyle@gmail.com>2018-10-20 21:51:14 -0600
committerKyle McFarland <tfkyle@gmail.com>2018-10-20 21:51:14 -0600
commit02ede72ce9ddeb4d7d7241503585bb07fe3e2c50 (patch)
tree0c3ce633cd75c9c4756aa4799ff17ca26b8f6b26
parentbfdc219f139bf9645f29b95bf4cc7824138f87b7 (diff)
downloadcoding-assignment-02ede72ce9ddeb4d7d7241503585bb07fe3e2c50.zip
coding-assignment-02ede72ce9ddeb4d7d7241503585bb07fe3e2c50.tar.gz
coding-assignment-02ede72ce9ddeb4d7d7241503585bb07fe3e2c50.tar.bz2
Allow suppliers to have multiple representatives
This adds a basic Representative model and allows for editing them in the admin interface both in the Suppliers admin panel for each supplier and in a new Representatives admin panel which allows bulk editing of all representatives. Currently multiple representatives are just listed in the component view as extra rows below the company row but it would probably make sense to add a view for viewing suppliers directly. The REST API has also been slightly modified to return a list of representatives for each supplier in the components endpoint, adding a suppliers endpoint would probably also be a good idea. Requires running: $ python manage.py migrate procurement 0002_add_representative To update the database for the new model, both forward and lossy reverse data migration is implemented in the migration.
-rw-r--r--procurement/admin.py15
-rw-r--r--procurement/admin_forms.py3
-rw-r--r--procurement/migrations/0002_add_representative.py80
-rw-r--r--procurement/models.py12
-rw-r--r--procurement/serializers.py8
-rw-r--r--procurement/templates/procurement/includes/supplier_list.html12
-rw-r--r--procurement/templates/procurement/source_components.html6
-rw-r--r--procurement/views.py9
8 files changed, 132 insertions, 13 deletions
diff --git a/procurement/admin.py b/procurement/admin.py
index 993b1b5..f35d706 100644
--- a/procurement/admin.py
+++ b/procurement/admin.py
@@ -2,13 +2,23 @@ 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
+from procurement.models import Supplier, Component, Representative
+class RepresentativeAdmin(admin.ModelAdmin):
+ list_display = ('name', 'email', 'supplier', 'updated')
+ ordering = ("supplier",)
+
+class RepresentativeInline(admin.TabularInline):
+ model = Representative
class SupplierAdmin(admin.ModelAdmin):
- list_display = ('name', 'representative_name', 'representative_email', 'is_authorized', 'updated')
+ list_display = ('name', 'get_representatives', 'is_authorized', 'updated')
filter_horizonal = ('components',)
+ inlines = [RepresentativeInline]
+ def get_representatives(self, obj):
+ return list(str(x) for x in obj.representatives.all())
+ get_representatives.short_description = "Representatives"
class ComponentAdmin(admin.ModelAdmin):
list_display = ('name', 'sku', 'updated')
@@ -27,5 +37,6 @@ class ComponentAdmin(admin.ModelAdmin):
})
+admin.site.register(Representative, RepresentativeAdmin)
admin.site.register(Supplier, SupplierAdmin)
admin.site.register(Component, ComponentAdmin)
diff --git a/procurement/admin_forms.py b/procurement/admin_forms.py
index b7bbd1c..018377e 100644
--- a/procurement/admin_forms.py
+++ b/procurement/admin_forms.py
@@ -3,7 +3,6 @@ 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),
@@ -17,4 +16,4 @@ class ComponentAdminForm(forms.ModelForm):
class Meta:
model = Component
- fields = ['name', 'sku', 'suppliers'] \ No newline at end of file
+ fields = ['name', 'sku', 'suppliers']
diff --git a/procurement/migrations/0002_add_representative.py b/procurement/migrations/0002_add_representative.py
new file mode 100644
index 0000000..86a2f38
--- /dev/null
+++ b/procurement/migrations/0002_add_representative.py
@@ -0,0 +1,80 @@
+# Generated by Django 2.1.2 on 2018-10-18 02:18
+
+from django.db import migrations, models
+import django.db.models.deletion
+import warnings
+
+def copy_reps_forward(apps, editor):
+ Supplier = apps.get_model("procurement", "Supplier")
+ Representative = apps.get_model("procurement", "Representative")
+ suppliers = Supplier.objects.all()
+ for sup in suppliers:
+ name = sup.representative_name
+ email = sup.representative_email
+ rep = Representative(name=name, email=email, supplier=sup)
+ rep.save()
+
+class ReverseMigrationDataLossWarning(UserWarning):
+ def __init__(self, supplier, kept_rep, lost_reps):
+ self.supplier = supplier
+ self.kept_rep = kept_rep
+ self.lost_reps = lost_reps
+
+ @staticmethod
+ def format_rep(rep):
+ return '{} <{}>'.format(rep.name, rep.email)
+
+ def __str__(self):
+ return "Supplier {} has multiple representatives, only keeping {}. {} will be lost".format(self.supplier.name, self.format_rep(self.kept_rep), [self.format_rep(rep) for rep in self.lost_reps])
+
+def copy_reps_rev(apps, editor):
+ warnings.filterwarnings("always", category=ReverseMigrationDataLossWarning)
+ Supplier = apps.get_model("procurement", "Supplier")
+ Representative = apps.get_model("procurement", "Representative")
+ suppliers = Supplier.objects.all()
+ for sup in suppliers:
+ reps = sup.representatives.all()
+ if reps:
+ keep = reps[0]
+ if len(reps) > 1:
+ lost = reps[1:]
+ warnings.warn(ReverseMigrationDataLossWarning(sup, keep, lost))
+ sup.representative_name = keep.name
+ sup.representative_email = keep.email
+ sup.save()
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('procurement', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Representative',
+ 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)),
+ ('email', models.CharField(max_length=255)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.AddField(
+ model_name='representative',
+ name='supplier',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='representatives', to='procurement.Supplier'),
+ ),
+ migrations.RunPython(copy_reps_forward, copy_reps_rev),
+ migrations.RemoveField(
+ model_name='supplier',
+ name='representative_email',
+ ),
+ migrations.RemoveField(
+ model_name='supplier',
+ name='representative_name',
+ ),
+ ]
diff --git a/procurement/models.py b/procurement/models.py
index 1e70736..43e5a07 100644
--- a/procurement/models.py
+++ b/procurement/models.py
@@ -51,19 +51,25 @@ class DashboardModel(models.Model):
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 Representative(DashboardModel):
+ """Model which represents a single Representative, each supplier
+ can have multiple representatives"""
+ name = models.CharField(max_length=255)
+ email = models.CharField(max_length=255)
+ supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE, related_name="representatives", null=True, blank=True)
+
+ def __str__(self):
+ return '{} <{}>'.format(self.name, self.email)
class Component(DashboardModel):
"""
diff --git a/procurement/serializers.py b/procurement/serializers.py
index 1840837..0466402 100644
--- a/procurement/serializers.py
+++ b/procurement/serializers.py
@@ -1,8 +1,14 @@
from rest_framework import serializers
-from procurement.models import Component, Supplier
+from procurement.models import Component, Supplier, Representative
+class RepresentativeSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Representative
+ exclude = ('created', 'updated', 'supplier', 'id')
class SupplierSerializer(serializers.ModelSerializer):
+ representatives = RepresentativeSerializer(many=True, read_only=True)
+
class Meta:
model = Supplier
exclude = ('created', 'updated')
diff --git a/procurement/templates/procurement/includes/supplier_list.html b/procurement/templates/procurement/includes/supplier_list.html
index 028e62c..ffd7435 100644
--- a/procurement/templates/procurement/includes/supplier_list.html
+++ b/procurement/templates/procurement/includes/supplier_list.html
@@ -17,11 +17,17 @@
</thead>
<tbody>
{% for supplier in supplier_results %}
+ {% for representative in supplier.representatives.all %}
<tr>
+ {% if forloop.first %}
<td>{{ supplier.name }}</td>
- <td>{{ supplier.representative_name }}</td>
- <td>{{ supplier.representative_email }}</td>
+ {% else %}
+ <td></td>
+ {% endif %}
+ <td>{{ representative.name }}</td>
+ <td>{{ representative.email }}</td>
</tr>
+ {% endfor %}
{% empty %}
<tr>
<td colspan="3">No authorized suppliers found.</td>
@@ -34,4 +40,4 @@
</div>
<!-- /.panel-body -->
</div>
-<!-- / Supplier Results Panel --> \ No newline at end of file
+<!-- / Supplier Results Panel -->
diff --git a/procurement/templates/procurement/source_components.html b/procurement/templates/procurement/source_components.html
index 3eebf84..db412a2 100644
--- a/procurement/templates/procurement/source_components.html
+++ b/procurement/templates/procurement/source_components.html
@@ -42,6 +42,10 @@
<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-users fa-fw"></i> {{ representative_count }} Representatives</span>
+ <span class="record-updated text-muted small"><em>{{ representatives_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>
@@ -58,4 +62,4 @@
{% 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
+{% endblock %}
diff --git a/procurement/views.py b/procurement/views.py
index 8891f89..1516455 100644
--- a/procurement/views.py
+++ b/procurement/views.py
@@ -1,7 +1,7 @@
from django.views.generic import FormView, TemplateView
from procurement.forms import ComponentSearchForm
-from procurement.models import Supplier, Component
+from procurement.models import Supplier, Component, Representative
class ComponentSearchView(FormView):
@@ -20,6 +20,11 @@ class ComponentSearchView(FormView):
suppliers_last_updated = ''
try:
+ representatives_last_updated = Representative.objects.latest('updated').time_since_update
+ except Representative.DoesNotExist:
+ representatives_last_updated = ''
+
+ try:
components_last_updated = Component.objects.latest('updated').time_since_update
except Component.DoesNotExist:
components_last_updated = ''
@@ -32,6 +37,8 @@ class ComponentSearchView(FormView):
'suppliers_last_updated': suppliers_last_updated,
'component_count': Component.objects.all().count(),
'components_last_updated': components_last_updated,
+ 'representative_count': Representative.objects.all().count(),
+ 'representatives_last_updated': representatives_last_updated,
})
return context