Add new ticket system models and more api work

 - nests ticket under correct product
 - ticket creation and update take product context into account
diff --git a/pyproject.toml b/pyproject.toml
index c0671f9..fee9aad 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,6 +10,7 @@
 django = "^3.2.2"
 django-rest-framework = "^0.1.0"
 drf-yasg = "^1.20.0"
+drf-nested-routers = "^0.93.3"
 
 [tool.poetry.dev-dependencies]
 selenium = "^3.141.0"
diff --git a/trackers/admin.py b/trackers/admin.py
index 2a4a378..8634528 100644
--- a/trackers/admin.py
+++ b/trackers/admin.py
@@ -19,7 +19,6 @@
 from django.contrib import admin
 
 # Register your models here.
-from trackers.models import Ticket, ChangeEvent
+from trackers.models import Product
 
-admin.site.register(Ticket)
-admin.site.register(ChangeEvent)
+admin.site.register(Product)
diff --git a/trackers/api/serializers.py b/trackers/api/serializers.py
index 92db9c6..a48abee 100644
--- a/trackers/api/serializers.py
+++ b/trackers/api/serializers.py
@@ -1,7 +1,7 @@
 from django.contrib.auth.models import User, Group
+from django.shortcuts import get_object_or_404
 from rest_framework import serializers
-from trackers import models
-from ..models import Product
+from ..models import Product, Ticket
 
 
 class UserSerializer(serializers.HyperlinkedModelSerializer):
@@ -23,41 +23,18 @@
 
 
 class TicketSerializer(serializers.ModelSerializer):
-    api_url = serializers.SerializerMethodField()
-    api_events_url = serializers.SerializerMethodField()
-
     class Meta:
-        model = models.Ticket
-        fields = '__all__'
+        model = Ticket
+        fields = (
+            'product_ticket_id',
+            'summary',
+            'description',
+        )
+        extra_kwargs = {'product_ticket_id': {'required': False}}
 
-    def get_api_url(self, obj):
-        return self.context['request'].build_absolute_uri(obj.api_url())
-
-    def get_api_events_url(self, obj):
-        return self.context['request'].build_absolute_uri(obj.api_events_url())
-
-
-class TicketFieldSerializer(serializers.ModelSerializer):
-    api_url = serializers.SerializerMethodField()
-
-    class Meta:
-        model = models.TicketField
-        fields = '__all__'
-
-    def get_api_url(self, obj):
-        return self.context['request'].build_absolute_uri(obj.api_url())
-
-
-class ChangeEventSerializer(serializers.ModelSerializer):
-    api_url = serializers.SerializerMethodField()
-    api_ticket_url = serializers.SerializerMethodField()
-
-    class Meta:
-        model = models.ChangeEvent
-        fields = '__all__'
-
-    def get_api_url(self, obj):
-        return self.context['request'].build_absolute_uri(obj.api_url())
-
-    def get_api_ticket_url(self, obj):
-        return self.context['request'].build_absolute_uri(obj.api_ticket_url())
+    def create(self, validated_data):
+        if 'prefix' not in self.context['view'].kwargs.keys():
+            prefix = self.context['view'].kwargs['product_prefix']
+            product = get_object_or_404(Product.objects.all(), prefix=prefix)
+            validated_data['product'] = product
+        return super().create(validated_data)
diff --git a/trackers/api/urls.py b/trackers/api/urls.py
index c9ccf2f..1ca3741 100644
--- a/trackers/api/urls.py
+++ b/trackers/api/urls.py
@@ -17,22 +17,33 @@
 
 from django.urls import path
 from django.conf.urls import include
-from rest_framework import routers
+from rest_framework_nested import routers
 from . import views
 
 router = routers.DefaultRouter()
 router.register('users', views.UserViewSet)
 router.register('groups', views.GroupViewSet)
 router.register('products', views.ProductViewSet)
-router.register('tickets', views.TicketViewSet)
 
-ticket_router = routers.DefaultRouter()
-ticket_router.register('ticketevents', views.ChangeEventViewSet)
+products_router = routers.NestedDefaultRouter(router, 'products', lookup='product')
+products_router.register('tickets', views.TicketViewSet, basename='product-tickets')
 
 urlpatterns = [
     path('', include(router.urls)),
-    path('tickets/<uuid:id>/', include(ticket_router.urls)),
-    path('swagger<str:format>', views.schema_view.without_ui(cache_timeout=0), name='schema-json'),
-    path('swagger/', views.schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
-    path('redoc/', views.schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
+    path('', include(products_router.urls)),
+    path(
+        'swagger<str:format>',
+        views.schema_view.without_ui(cache_timeout=0),
+        name='schema-json',
+    ),
+    path(
+        'swagger/',
+        views.schema_view.with_ui('swagger', cache_timeout=0),
+        name='schema-swagger-ui',
+    ),
+    path(
+        'redoc/',
+        views.schema_view.with_ui('redoc', cache_timeout=0),
+        name='schema-redoc',
+    ),
 ]
diff --git a/trackers/api/views.py b/trackers/api/views.py
index 34f0d57..a1d439a 100644
--- a/trackers/api/views.py
+++ b/trackers/api/views.py
@@ -16,12 +16,13 @@
 #  under the License.
 
 from django.contrib.auth.models import User, Group
+from django.shortcuts import get_object_or_404
 from drf_yasg.views import get_schema_view
 from drf_yasg import openapi
-from rest_framework import permissions, viewsets
+from rest_framework import permissions, status, viewsets
+from rest_framework.response import Response
 from . import serializers
-from ..models import Product
-from trackers import models
+from .. import models
 
 
 schema_view = get_schema_view(
@@ -45,20 +46,16 @@
 
 
 class ProductViewSet(viewsets.ModelViewSet):
-    queryset = Product.objects.all()
+    queryset = models.Product.objects.all()
     serializer_class = serializers.ProductSerializer
-
-
-class TicketFieldViewSet(viewsets.ModelViewSet):
-    queryset = models.TicketField.objects.all()
-    serializer_class = serializers.TicketFieldSerializer
+    lookup_field = 'prefix'
 
 
 class TicketViewSet(viewsets.ModelViewSet):
     queryset = models.Ticket.objects.all()
     serializer_class = serializers.TicketSerializer
+    lookup_field = 'product_ticket_id'
 
-
-class ChangeEventViewSet(viewsets.ModelViewSet):
-    queryset = models.ChangeEvent.objects.all()
-    serializer_class = serializers.ChangeEventSerializer
+    def get_queryset(self, *args, **kwargs):
+        prefix = self.kwargs['product_prefix']
+        return models.Ticket.objects.filter(product=prefix)
diff --git a/trackers/models.py b/trackers/models.py
index f7fb321..6b48f0e 100644
--- a/trackers/models.py
+++ b/trackers/models.py
@@ -38,6 +38,7 @@
 
 class ProductConfig(models.Model):
     """Possibly legacy table - keeping for now"""
+
     product = models.ForeignKey(Product, on_delete=models.CASCADE)
     section = models.TextField()
     option = models.TextField()
@@ -50,6 +51,7 @@
 
 class ProductResourceMap(models.Model):
     """Possibly legacy model - keeping for now"""
+
     product_id = models.ForeignKey(Product, on_delete=models.CASCADE)
     resource_type = models.TextField(blank=True, null=True)
     resource_id = models.TextField(blank=True, null=True)
@@ -58,68 +60,143 @@
         db_table = 'bloodhound_productresourcemap'
 
 
-class ModelCommon(models.Model):
-    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
-    created = models.DateTimeField(auto_now_add=True, editable=False)
+class Component(models.Model):
+    name = models.TextField(primary_key=True)
+    owner = models.TextField(blank=True, null=True)
+    description = models.TextField(blank=True, null=True)
+    product = models.ForeignKey(Product, on_delete=models.PROTECT)
 
     class Meta:
-        abstract = True
+        db_table = 'component'
+        unique_together = (('name', 'product'),)
 
 
-class Ticket(ModelCommon):
+class Enum(models.Model):
+    type = models.TextField(primary_key=True)
+    name = models.TextField()
+    value = models.TextField(blank=True, null=True)
+    product = models.ForeignKey(Product, on_delete=models.PROTECT)
 
-    def api_url(self):
-        return reverse('ticket-detail', args=(self.id,))
-
-    def api_events_url(self):
-        return reverse('changeevent-list', args=(self.id,))
-
-    def last_update(self):
-        last_event = self.changeevent_set.order_by('created').last()
-        return self.created if last_event is None else last_event.created
-
-    def add_field_event(self, field, newvalue):
-        current_lines = self.get_field_value(field).splitlines(keepends=True)
-        replace_lines = newvalue.splitlines(keepends=True)
-        result = '\n'.join(difflib.ndiff(current_lines, replace_lines))
-
-        tfield, created = TicketField.objects.get_or_create(name=field)
-        c = ChangeEvent(ticket=self, field=tfield, diff=result)
-        c.save()
-
-    def get_field_value(self, field):
-        try:
-            tfield = TicketField.objects.get(name=field)
-        except TicketField.DoesNotExist as e:
-            return ''
-
-        event = self.changeevent_set.filter(field=tfield).order_by('created').last()
-        return '' if event is None else event.value()
+    class Meta:
+        db_table = 'enum'
+        unique_together = (('type', 'name', 'product'),)
 
 
-class TicketField(ModelCommon):
-    name = models.CharField(max_length=32)
+class Milestone(models.Model):
+    name = models.TextField(primary_key=True)
+    due = models.BigIntegerField(blank=True, null=True)
+    completed = models.BigIntegerField(blank=True, null=True)
+    description = models.TextField(blank=True, null=True)
+    product = models.ForeignKey(Product, on_delete=models.PROTECT)
 
-    def api_url(self):
-        return reverse('ticketfield-detail', args=(self.id,))
+    class Meta:
+        db_table = 'milestone'
+        unique_together = (('name', 'product'),)
 
 
-class ChangeEvent(ModelCommon):
-    ticket = models.ForeignKey(Ticket, models.CASCADE, null=False)
-    field = models.ForeignKey(TicketField, models.CASCADE)
-    diff = models.TextField()
+class Version(models.Model):
+    name = models.TextField(primary_key=True)
+    time = models.BigIntegerField(blank=True, null=True)
+    description = models.TextField(blank=True, null=True)
+    product = models.ForeignKey(Product, on_delete=models.PROTECT)
 
-    def value(self, which=2):
-        return ''.join(difflib.restore(self.diff.splitlines(keepends=True), which)).strip()
+    class Meta:
+        db_table = 'version'
+        unique_together = (('name', 'product'),)
 
-    old_value = functools.partialmethod(value, which=1)
 
-    def __str__(self):
-        return "Change to: {}; Field: {}; Diff: {}".format(
-            self.ticket, self.field, self.diff)
+class Ticket(models.Model):
+    uid = models.AutoField(primary_key=True)
+    type = models.ForeignKey(
+        Enum,
+        blank=True,
+        null=True,
+        on_delete=models.PROTECT,
+        related_name='%(app_label)s_%(class)s_type_related',
+    )
+    time = models.BigIntegerField(blank=True, null=True)
+    changetime = models.BigIntegerField(blank=True, null=True)
+    component = models.ForeignKey(
+        Component, on_delete=models.PROTECT, blank=True, null=True
+    )
+    severity = models.TextField(blank=True, null=True)
+    priority = models.TextField(blank=True, null=True)
+    owner = models.TextField(blank=True, null=True)
+    reporter = models.TextField(blank=True, null=True)
+    cc = models.TextField(blank=True, null=True)
+    version = models.ForeignKey(
+        Version, on_delete=models.PROTECT, blank=True, null=True
+    )
+    milestone = models.ForeignKey(
+        Milestone, on_delete=models.PROTECT, blank=True, null=True
+    )
+    status = models.TextField(blank=True, null=True)
+    resolution = models.ForeignKey(
+        Enum,
+        on_delete=models.PROTECT,
+        related_name='%(app_label)s_%(class)s_resolution_related',
+        blank=True,
+        null=True,
+    )
+    summary = models.TextField()
+    description = models.TextField(blank=True, null=True)
+    keywords = models.TextField(blank=True, null=True)
+    product = models.ForeignKey(Product, on_delete=models.PROTECT)
+    product_ticket_id = models.IntegerField(db_column='id', editable=False)
 
-    def api_url(self):
-        return reverse('changeevent-detail', args=(self.ticket.id, self.id,))
+    class Meta:
+        db_table = 'ticket'
+        unique_together = (('product', 'product_ticket_id'),)
 
-    def api_ticket_url(self):
-        return reverse('ticket-detail', args=(self.ticket.id,))
+    def save(self, *args, **kwargs):
+        if self._state.adding:
+            # FIXME: deleting the latest tickets will allow reuse
+            # Consider:
+            #     disallowing deletion
+            #     switching to uuids
+            #     recording last used on product model
+            product_tickets = Ticket.objects.filter(product=self.product)
+            if product_tickets.exists():
+                newest = product_tickets.latest('product_ticket_id')
+                new_id = 1 + newest.product_ticket_id
+            else:
+                new_id = 1
+            self.product_ticket_id = new_id
+        super().save(*args, **kwargs)
+
+
+class TicketChange(models.Model):
+    ticket = models.ForeignKey(Ticket, on_delete=models.PROTECT)
+    time = models.BigIntegerField()
+    author = models.TextField(blank=True, null=True)
+    field = models.TextField()
+    oldvalue = models.TextField(blank=True, null=True)
+    newvalue = models.TextField(blank=True, null=True)
+    product = models.ForeignKey(Product, on_delete=models.PROTECT)
+
+    class Meta:
+        db_table = 'ticket_change'
+        unique_together = (('ticket', 'time', 'field', 'product'),)
+
+
+class TicketCustom(models.Model):
+    ticket = models.ForeignKey(Ticket, on_delete=models.PROTECT)
+    name = models.TextField()
+    value = models.TextField(blank=True, null=True)
+    product = models.ForeignKey(Product, on_delete=models.PROTECT)
+
+    class Meta:
+        db_table = 'ticket_custom'
+        unique_together = (('ticket', 'name', 'product'),)
+
+
+class Report(models.Model):
+    author = models.TextField(blank=True, null=True)
+    title = models.TextField(blank=True, null=True)
+    query = models.TextField(blank=True, null=True)
+    description = models.TextField(blank=True, null=True)
+    product = models.ForeignKey(Product, on_delete=models.PROTECT)
+
+    class Meta:
+        db_table = 'report'
+        unique_together = (('id', 'product'),)
diff --git a/trackers/tests/test_models.py b/trackers/tests/test_models.py
index 13191fc..fef8144 100644
--- a/trackers/tests/test_models.py
+++ b/trackers/tests/test_models.py
@@ -16,11 +16,11 @@
 #  under the License.
 
 from django.test import TestCase
-from ..models import Product
+from ..models import Product, Ticket
 
 
 class ProductTest(TestCase):
-    """Test modules for Product model"""
+    """Tests for Product model"""
     def setUp(self):
         Product.objects.create(
             prefix='BHD',
@@ -39,3 +39,44 @@
 
         self.assertEqual(bhd.name, "Bloodhound Legacy")
         self.assertEqual(bh.name, "Bloodhound")
+
+
+class TicketTest(TestCase):
+    """Test for Ticket model"""
+    def setUp(self):
+        self.product = Product.objects.create(
+            prefix='BH',
+            name='Bloodhound',
+            description='Apache Bloodhound',
+        )
+
+    def test_ticket_create_sets_product_ticket_number(self):
+        ticket = Ticket.objects.create(
+            product=self.product,
+        )
+        self.assertIsNotNone(ticket.product_ticket_id)
+
+    def test_ticket_create_sets_unique_product_ticket_number(self):
+        ticket1 = Ticket.objects.create(
+            product=self.product,
+        )
+        ticket2 = Ticket.objects.create(
+            product=self.product,
+        )
+        self.assertNotEqual(ticket1.product_ticket_id, ticket2.product_ticket_id)
+
+    def test_ticket_create_uses_unique_product_ticket_number_when_tickets_deleted(self):
+        ticket1 = Ticket.objects.create(
+            product=self.product,
+        )
+        ticket2 = Ticket.objects.create(
+            product=self.product,
+        )
+        ticket1.delete()
+        ticket3 = Ticket.objects.create(
+            product=self.product,
+        )
+        self.assertIsNotNone(ticket1.product_ticket_id)
+        self.assertIsNotNone(ticket2.product_ticket_id)
+        self.assertIsNotNone(ticket3.product_ticket_id)
+        self.assertNotEqual(ticket2.product_ticket_id, ticket3.product_ticket_id)
diff --git a/trackers/tests/tests.py b/trackers/tests/tests.py
index ef6bcb4..23e10d7 100644
--- a/trackers/tests/tests.py
+++ b/trackers/tests/tests.py
@@ -33,61 +33,3 @@
         self.assertTrue(response.content.startswith(b'<html>'))
         self.assertIn(b'<title>Bloodhound Trackers</title>', response.content)
         self.assertTrue(response.content.endswith(b'</html>'))
-
-
-from ..models import Ticket
-
-class TicketModelTest(TestCase):
-    def test_last_update_on_create_returns_created_date(self):
-        t = Ticket()
-        t.save()
-        self.assertEqual(t.created, t.last_update())
-
-    def test_last_update_returns_last_change_date(self):
-        # test may be safer with a fixture with an existing ticket to check
-        t = Ticket()
-        t.save()
-        t.add_field_event('summary', "this is the summary")
-        self.assertNotEqual(t.created, t.last_update())
-
-    def test_ticket_creation(self):
-        # Currently simple but may need updates for required fields
-        pre_count = Ticket.objects.count()
-        t = Ticket()
-        t.save()
-        self.assertEqual(pre_count + 1, Ticket.objects.count())
-
-    def test_ticket_add_field_event(self):
-        field = 'summary'
-        field_value = "this is the summary"
-
-        t = Ticket()
-        t.save()
-        t.add_field_event(field, field_value)
-
-        self.assertEqual(t.get_field_value(field), field_value)
-
-    def test_ticket_add_two_single_line_field_events_same_field(self):
-        field = 'summary'
-        first_field_value = "this is the summary"
-        second_field_value = "this is the replacement summary"
-
-        t = Ticket()
-        t.save()
-        t.add_field_event(field, first_field_value)
-        t.add_field_event(field, second_field_value)
-
-        self.assertEqual(t.get_field_value(field), second_field_value)
-
-    def test_ticket_add_two_multiline_field_events_same_field(self):
-        field = 'summary'
-        first_field_value = "this is the summary\nwith multiple lines"
-        second_field_value = "this is the replacement summary\nwith multiple lines"
-
-        t = Ticket()
-        t.save()
-        t.add_field_event(field, first_field_value)
-        t.add_field_event(field, second_field_value)
-
-        self.assertEqual(t.get_field_value(field), second_field_value)
-