Skip to content
Permalink
Browse files
Add new ticket system models and more api work
 - nests ticket under correct product
 - ticket creation and update take product context into account
  • Loading branch information
Gary Martin committed May 16, 2021
1 parent dc439b7 commit e3c1baadd8d9b6dd09bc0f3e355aaf70c5f4f49e
Showing 8 changed files with 214 additions and 169 deletions.
@@ -10,6 +10,7 @@ python = "^3.6"
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"
@@ -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)
@@ -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 Meta:


class TicketSerializer(serializers.ModelSerializer):
api_url = serializers.SerializerMethodField()
api_events_url = serializers.SerializerMethodField()

class Meta:
model = models.Ticket
fields = '__all__'

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())
model = Ticket
fields = (
'product_ticket_id',
'summary',
'description',
)
extra_kwargs = {'product_ticket_id': {'required': False}}

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)
@@ -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',
),
]
@@ -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 GroupViewSet(viewsets.ModelViewSet):


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)
@@ -38,6 +38,7 @@ class Meta:

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 Meta:

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 @@ class Meta:
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


class Ticket(ModelCommon):
db_table = 'component'
unique_together = (('name', 'product'),)

def api_url(self):
return reverse('ticket-detail', args=(self.id,))

def api_events_url(self):
return reverse('changeevent-list', args=(self.id,))
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 last_update(self):
last_event = self.changeevent_set.order_by('created').last()
return self.created if last_event is None else last_event.created
class Meta:
db_table = 'enum'
unique_together = (('type', 'name', 'product'),)

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()
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 get_field_value(self, field):
try:
tfield = TicketField.objects.get(name=field)
except TicketField.DoesNotExist as e:
return ''
class Meta:
db_table = 'milestone'
unique_together = (('name', 'product'),)

event = self.changeevent_set.filter(field=tfield).order_by('created').last()
return '' if event is None else event.value()

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)

class TicketField(ModelCommon):
name = models.CharField(max_length=32)
class Meta:
db_table = 'version'
unique_together = (('name', 'product'),)


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('ticketfield-detail', args=(self.id,))
class Meta:
db_table = 'ticket'
unique_together = (('product', 'product_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 ChangeEvent(ModelCommon):
ticket = models.ForeignKey(Ticket, models.CASCADE, null=False)
field = models.ForeignKey(TicketField, models.CASCADE)
diff = models.TextField()

def value(self, which=2):
return ''.join(difflib.restore(self.diff.splitlines(keepends=True), which)).strip()
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)

old_value = functools.partialmethod(value, which=1)
class Meta:
db_table = 'ticket_custom'
unique_together = (('ticket', 'name', 'product'),)

def __str__(self):
return "Change to: {}; Field: {}; Diff: {}".format(
self.ticket, self.field, self.diff)

def api_url(self):
return reverse('changeevent-detail', args=(self.ticket.id, self.id,))
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)

def api_ticket_url(self):
return reverse('ticket-detail', args=(self.ticket.id,))
class Meta:
db_table = 'report'
unique_together = (('id', 'product'),)

0 comments on commit e3c1baa

Please sign in to comment.