-
-
-
- ID |
- User |
- First Name |
- Last Name |
- Role |
- SuperUser |
- Email |
-
-
-
- {% for user in users %}
-
- {{ user.id }} |
- {{ user.username }} |
- {{ user.first_name }} |
- {{ user.last_name }} |
- placeholder |
- {{ user.is_superuser }} |
- {{ user.email }} |
-
- {% endfor %}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ID |
+ User |
+ First Name |
+ Last Name |
+ Email |
+
+
+
+ {% for submitter in submitters %}
+
+ {{ submitter.id }} |
+ {{ submitter.username }} |
+ {{ submitter.first_name }} |
+ {{ submitter.last_name }} |
+
+ {{ submitter.email }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ID |
+ User |
+ First Name |
+ Last Name |
+ Email |
+
+
+
+ {% for staff_member in staff %}
+
+ {{ staff_member.id }} |
+ {{ staff_member.username }} |
+ {{ staff_member.first_name }} |
+ {{ staff_member.last_name }} |
+
+ {{ staff_member.email }} |
+
+ {% endfor %}
+
+
+
+
{% endblock %}
{% block scripts %}
-
+
-
{% endblock %}
diff --git a/accounts/urls.py b/accounts/urls.py
index b343126..25e17de 100644
--- a/accounts/urls.py
+++ b/accounts/urls.py
@@ -3,7 +3,6 @@
from accounts.views import index, logout, login, registration, user_profile, user_list
urlpatterns = [
- # url(r'^user_list/$', views.user_list, name='user_list'),
url(r'^logout/$', logout, name='logout'),
url(r'^login/$', login, name='login'),
url(r'^register/$', registration, name='registration'),
diff --git a/accounts/views.py b/accounts/views.py
index 7521e65..0559bd0 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -31,7 +31,8 @@ def login(request):
user = auth.authenticate(username=request.POST['username'],
password=request.POST['password'])
if user:
- messages.success(request, "You have successfully logged in.")
+ messages.success(
+ request, "Welcome, " + user.username + ". You have been successfully logged in.")
auth.login(user=user, request=request)
return redirect(reverse('tickets'))
else:
@@ -88,3 +89,7 @@ def user_list(request):
return render(request, 'user_list.html', {"users": users,
"staff": staff,
"submitters": submitters})
+
+# @login_required()
+# def update_email(request):
+# """Updates User Email"""
diff --git a/issue_tracker/settings.py b/issue_tracker/settings.py
index 5d138bf..a5cac9e 100644
--- a/issue_tracker/settings.py
+++ b/issue_tracker/settings.py
@@ -11,6 +11,9 @@
"""
import os
+import dj_database_url
+import env
+from django.contrib.messages import constants as messages
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -36,12 +39,16 @@
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
+ 'livereload',
'django.contrib.staticfiles',
'django_forms_bootstrap',
'accounts',
'tickets',
'taggit',
'simple_history',
+ 'rest_framework',
+ 'crispy_forms',
+ 'checkout',
]
MIDDLEWARE = [
@@ -53,6 +60,7 @@
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
+ 'livereload.middleware.LiveReloadScript',
]
ROOT_URLCONF = 'issue_tracker.urls'
@@ -79,14 +87,19 @@
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
+# sqlite3
+# DATABASES = {
+# 'default': {
+# 'ENGINE': 'django.db.backends.sqlite3',
+# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+# }
+# }
+
+# postgresql
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
- }
+ 'default': dj_database_url.parse("postgres://hduoshlpgoxoyi:fa43c8f4059c34a7cbe4d45c3443200db5fbe8b956f313816e75a9c9d7668d50@ec2-54-195-247-108.eu-west-1.compute.amazonaws.com:5432/d7orbajbrr4b8i")
}
-
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
@@ -131,3 +144,19 @@
MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"
TAGGIT_CASE_INSENSITIVE = True
+
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+MEDIA_URL = '/media/'
+
+CRISPY_TEMPLATE_PACK = 'bootstrap4'
+
+STRIPE_PUBLISHABLE = os.getenv('STRIPE_PUBLISHABLE')
+STRIPE_SECRET = os.getenv('STRIPE_SECRET')
+
+MESSAGE_TAGS = {
+ messages.DEBUG: 'alert-info',
+ messages.INFO: 'alert-info',
+ messages.SUCCESS: 'alert-success',
+ messages.WARNING: 'alert-warning',
+ messages.ERROR: 'alert-danger',
+}
diff --git a/issue_tracker/urls.py b/issue_tracker/urls.py
index 2f6affe..3a015c2 100644
--- a/issue_tracker/urls.py
+++ b/issue_tracker/urls.py
@@ -15,18 +15,18 @@
"""
from django.conf.urls import url, include
from django.contrib import admin
-from accounts.views import index, logout, login, registration, user_profile, user_list
+from django.conf import settings
+from django.conf.urls.static import static
+from accounts.views import index
urlpatterns = [
- # url(r'^$', admin.site.urls),
url(r'^$', index, name='index'),
url(r'^admin/', admin.site.urls),
url(r'^tickets/', include('tickets.urls')),
- # move the below into accounts?
url(r'^accounts/', include('accounts.urls')),
- # url(r'^accounts/logout/$', logout, name='logout'),
- # url(r'^accounts/login/$', login, name='login'),
- # url(r'^accounts/register/$', registration, name='registration'),
- # url(r'^accounts/profile/$', user_profile, name='profile'),
- # url(r'^accounts/user_list/$', user_list, name='user_list'),
+ url(r'^checkout/', include('checkout.urls')),
]
+
+if settings.DEBUG:
+ urlpatterns += static(settings.MEDIA_URL,
+ document_root=settings.MEDIA_ROOT)
diff --git a/requirements.txt b/requirements.txt
index 7605570..26a5de6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,27 @@
+asgiref==3.2.5
autopep8==1.5
-Django==1.11.24
+beautifulsoup4==4.8.2
+certifi==2019.11.28
+chardet==3.0.4
+coverage==5.0.4
+dj-database-url==0.5.0
+Django==1.11.28
+django-crispy-forms==1.9.0
django-forms-bootstrap==3.1.0
+django-livereload-server==0.3.2
+django-simple-history==2.8.0
+django-taggit==1.2.0
+djangorestframework==3.11.0
+gunicorn==20.0.4
+idna==2.9
+Pillow==7.0.0
+psycopg2==2.7.3.2
pycodestyle==2.5.0
pytz==2019.3
+requests==2.23.0
+six==1.14.0
+soupsieve==2.0
+sqlparse==0.3.1
+stripe==2.43.0
+tornado==6.0.3
+urllib3==1.25.8
diff --git a/static/css/style.css b/static/css/style.css
index ba426cb..c3c0b0a 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -24,7 +24,7 @@ html {
}
.kanban-container {
- max-width: 1400px;
+ max-width: 1600px;
}
.user-list-container,
@@ -36,6 +36,10 @@ html {
max-width: 1700px;
}
+#hide-cancelled-checkbox {
+ font-size: 15px;
+}
+
/* Pie Chart and Row Chart labels */
.pie-label-group text,
g .row {
@@ -61,28 +65,34 @@ footer {
padding-bottom: 32px;
}
+/* Update Status Dropdown */
+.dropdown-toggle {
+ opacity: 0.7;
+}
+
.kanban-col {
- margin: 0px;
- padding: 0px;
+ margin: 3px;
+ padding: 3px;
}
.kanban-col .jumbotron {
padding: 6px;
}
-.kanban-todo {
- background-color: white;
+.kanban-new .card {
+ background-color: lightblue;
}
-.kanban-resolved {
- background-color: white;
+
+.kanban-in-progress .card {
+ background-color: lightgoldenrodyellow;
}
-.kanban-in-progress {
- background-color: white;
+.kanban-resolved .card {
+ background-color: #c3e6cb;
}
-.kanban-cancelled {
- background-color: white;
+.kanban-cancelled .card {
+ background-color: lightgrey;
}
/* From https://github.com/masagameplay/light-bootstrap-colors/blob/master/lightBSColors.css */
diff --git a/static/js/base.js b/static/js/base.js
index b3b646b..3298ed9 100644
--- a/static/js/base.js
+++ b/static/js/base.js
@@ -5,7 +5,7 @@ $(document).ready(function() {
});
// KANBAN JS
- if (window.location.pathname == '/kanban/') {
+ if (window.location.pathname == '/tickets/kanban/') {
// Toggle Cancelled column display
$('#cancelled-checkbox').click(function() {
$('.kanban-cancelled')
diff --git a/static/js/dashboard.js b/static/js/dashboard.js
index 755ae30..5c9f824 100644
--- a/static/js/dashboard.js
+++ b/static/js/dashboard.js
@@ -9,25 +9,131 @@ fetch('http://localhost:8000/tickets/api/tickets')
})
.then(data => {
console.log(data);
+
+ // date parsing test
+ // const dateFormatSpecifier = '%m/%d/%Y';
+ const dateFormatSpecifier = '%Y-%m-%dT%H:%M:%S.%f%Z';
+ const dateFormat = d3.timeFormat(dateFormatSpecifier);
+ const dateFormatParser = d3.timeParse(dateFormatSpecifier);
+ const numberFormat = d3.format('.2f');
+
+ data.forEach(d => {
+ // CREATED DATE parsed
+ d.created_date_dd = dateFormatParser(d.created_date);
+ // Day month year
+ d.created_date_day = d3.timeDay(d.created_date_dd);
+ // Month
+ d.created_date_month = d3.timeMonth(d.created_date_dd);
+ // Month
+ d.created_date_year = d3.timeYear(d.created_date_dd);
+ // RESOLVED DATE parsed, if resolved
+ if (d.resolved_date) {
+ // Parsed date
+ d.resolved_date_dd = dateFormatParser(d.resolved_date);
+ // Day month year
+ d.resolved_date_day = d3.timeDay(d.resolved_date_dd);
+ // Month
+ d.resolved_date_month = d3.timeMonth(d.resolved_date_dd);
+ // Year
+ d.resolved_date_year = d3.timeYear(d.resolved_date_dd);
+ }
+ });
+
drawGraphs(data);
});
function drawGraphs(data) {
let ndx = crossfilter(data);
- drawTicketTypePieChart(ndx);
+ drawTicketTypeRowChart(ndx);
drawPriorityPieChart(ndx);
- drawAssignedToRowChart(ndx);
- drawStatusPieChart(ndx);
+ drawStatusRowChart(ndx);
drawUpvotesRowChart(ndx);
drawStatusByMonthBarChart(ndx);
+ showFilteredCount(ndx);
+ showAverageDaysToResolve(ndx);
// drawDataTable(ndx);
+ // showAgePriorityScatterPlot(ndx);
+ // test, not useful?
+ // showAgedRowChart(ndx);
dc.renderAll();
}
+// Not useful?
+function showAgedRowChart(ndx) {
+ let agedDim = ndx.dimension(d => d.age);
+ let agedGroup = agedDim.group();
+
+ dc.rowChart('#agedRowChart')
+ .width(500)
+ .height(130)
+ .gap(2)
+ // .renderTitleLabel(true)
+ .titleLabelOffsetX(413)
+ .label(d => d.key + ': ' + d.value)
+ // .rowsCap(5)
+ .useViewBoxResizing(true)
+ .dimension(agedDim)
+ .group(agedGroup);
+}
+
+// Average days until resolution
+function showAverageDaysToResolve(ndx) {
+ // Custom reduce Average function
+ function reduceAvg(dimension, type) {
+ return dimension.groupAll().reduce(
+ function(p, v) {
+ p.count++;
+ p.total += v[type];
+ p.average = p.total / p.count;
+ return p;
+ },
+
+ function(p, v) {
+ p.count--;
+ p.total -= v[type];
+ p.average = p.total / p.count;
+ return p;
+ },
+
+ function() {
+ return {
+ count: 0,
+ total: 0,
+ average: 0
+ };
+ }
+ );
+ }
+ // groupAll averages groups
+ // let ageGroup = reduceAvg(ndx, 'age');
+ let ageGroup = reduceAvg(ndx, 'days_to_resolve');
+ dc.numberDisplay('#average-days-to-resolve')
+ // .group(group)
+ .group(ageGroup)
+ .valueAccessor(function(d) {
+ return d.average;
+ });
+}
+
+// Test open tickets count
+function displayOpenTicketsCount(ndx) {
+ // let openDim = ndx.dimension(d => d.status);
+ let openGroup = ndx.groupAll();
+ print_filter(openGroup);
+
+ dc.numberDisplay('#open-tickets-count')
+ .group(openGroup)
+ .valueAccessor(function(d) {
+ if (d.status == 'Resolved') {
+ return d;
+ }
+ });
+}
+
// Status by month
function drawStatusByMonthBarChart(ndx) {
- var dateCreatedDim = ndx.dimension(d => d.created_date.substring(0, 10));
- var statusGroup = dateCreatedDim
+ let dateCreatedDim = ndx.dimension(d => d.created_date_day);
+ let statusGroup = dateCreatedDim
.group()
.reduce(reduceAdd, reduceRemove, reduceInitial);
@@ -43,63 +149,54 @@ function drawStatusByMonthBarChart(ndx) {
return {};
}
- print_filter(statusGroup);
-
// Stacked bar chart status by month
- dc.barChart('#statusByMonthBarChart')
- .width(470)
- .height(350)
+ stackedBar = dc
+ .barChart('#statusByMonthBarChart')
+ .width(800)
+ .height(375)
.dimension(dateCreatedDim)
.group(statusGroup, 'New', d => d.value['New'])
.stack(statusGroup, 'In Progress', d => d.value['In Progress'])
.stack(statusGroup, 'Resolved', d => d.value['Resolved'])
- .xAxisLabel('Date', 25)
+ .stack(statusGroup, 'Cancelled', d => d.value['Cancelled'])
+ .xAxisLabel('Date Submitted', 25)
.yAxisLabel('Number of Tickets', 25)
.useViewBoxResizing(true)
- .xUnits(dc.units.ordinal)
.renderHorizontalGridLines(true)
- .ordinalColors(['grey', 'orange', 'green'])
- .gap(60)
+ .ordinalColors(['lightblue', 'orange', 'green', 'grey'])
.renderTitle(true)
.title(function(d) {
return [
d.key + '\n',
'New: ' + (d.value['New'] || '0'),
'In Progress: ' + (d.value['In Progress'] || '0'),
- 'Resolved: ' + (d.value['Resolved'] || '0')
+ 'Resolved: ' + (d.value['Resolved'] || '0'),
+ 'Cancelled: ' + (d.value['Cancelled'] || '0')
].join('\n');
})
.margins({ top: 30, left: 60, right: 20, bottom: 70 })
- .x(d3.scaleOrdinal());
+ .x(d3.scaleTime().domain([new Date(2020, 01, 15), new Date(2020, 03, 03)]))
+ .elasticX(true)
+ .alwaysUseRounding(true)
+ .xUnits(d3.timeDays)
+ .xAxis();
}
// TicketType Pie Chart
-function drawTicketTypePieChart(ndx) {
+function drawTicketTypeRowChart(ndx) {
let ticketTypeDim = ndx.dimension(d => d.ticket_type);
let ticketTypeGroup = ticketTypeDim.group();
- // Pie chart
- dc.pieChart('#ticketTypePieChart')
- .radius(120)
- .minAngleForLabel(0.2)
- .dimension(ticketTypeDim)
- .group(ticketTypeGroup)
- .ordinalColors(['#0D324D', '#73EEDC'])
- .height(295)
+ dc.rowChart('#ticketTypeRowChart')
.width(500)
+ .height(130)
+ .gap(2)
+ .titleLabelOffsetX(413)
.label(d => d.key + ': ' + d.value)
- // .cx(330)
- // .cy(150)
- // .legend(
- // dc
- // .legend()
- // .x(30)
- // .y(65)
- // .autoItemWidth(true)
- // .itemHeight(32)
- // .gap(12)
- // )
- .useViewBoxResizing(true);
+ .rowsCap(8)
+ .useViewBoxResizing(true)
+ .dimension(ticketTypeDim)
+ .group(ticketTypeGroup);
}
// Priority Pie Chart
@@ -107,57 +204,87 @@ function drawPriorityPieChart(ndx) {
let priorityDim = ndx.dimension(d => d.priority);
let priorityGroup = priorityDim.group();
- // Priority Pie chart
- dc.pieChart('#priorityPieChart')
- .radius(120)
- .minAngleForLabel(0.2)
- .dimension(priorityDim)
- .group(priorityGroup)
- .ordinalColors(['green', 'orange', 'red'])
- .height(295)
+ // Priority row Chart
+ dc.rowChart('#priorityRowChart')
.width(500)
+ .height(130)
+ .gap(2)
+ .titleLabelOffsetX(413)
.label(d => d.key + ': ' + d.value)
- // .cx(330)
- // .cy(150)
- // .legend(
- // dc
- // .legend()
- // .x(30)
- // .y(65)
- // .autoItemWidth(true)
- // .itemHeight(32)
- // .gap(12)
- // )
- .useViewBoxResizing(true);
+ .useViewBoxResizing(true)
+ .dimension(priorityDim)
+ .group(priorityGroup);
}
// Status Pie Chart
-function drawStatusPieChart(ndx) {
+function drawStatusRowChart(ndx) {
let statusDim = ndx.dimension(d => d.status);
let statusGroup = statusDim.group();
- // Pie chart
- dc.pieChart('#statusPieChart')
- .radius(120)
- .minAngleForLabel(0.2)
- .dimension(statusDim)
- .group(statusGroup)
- .ordinalColors(['grey', 'orange', 'green'])
- .height(295)
+ // Open/Closed
+ let openClosedDim = ndx.dimension(function(d) {
+ if (d.status == 'Resolved' || d.status == 'Cancelled') {
+ return 'Closed';
+ } else {
+ return 'Open';
+ }
+ });
+ let openClosedGroup = openClosedDim.group();
+
+ print_filter(statusGroup);
+ print_filter(openClosedGroup);
+
+ // Status Row Chart
+ dc.rowChart('#statusRowChart')
.width(500)
+ .height(200)
+ .gap(2)
+ .titleLabelOffsetX(413)
.label(d => d.key + ': ' + d.value)
- // .cx(330)
- // .cy(150)
- // .legend(
- // dc
- // .legend()
- // .x(30)
- // .y(65)
- // .autoItemWidth(true)
- // .itemHeight(32)
- // .gap(12)
- // )
- .useViewBoxResizing(true);
+ .ordinalColors(['green', 'orange', 'lightblue', 'grey'])
+ .useViewBoxResizing(true)
+ .dimension(statusDim)
+ .group(statusGroup);
+
+ // Open/Closed Status Pie Chart
+ dc.pieChart('#open-closed-tickets-pie-chart')
+ // .radius(40)
+ .minAngleForLabel(0.2)
+ .dimension(openClosedDim)
+ .group(openClosedGroup)
+ // .ordinalColors(['orange', 'green'])
+ .useViewBoxResizing(true)
+ .height(170)
+ // .width(800)
+ .label(d => d.key + ': ' + d.value);
+ // .innerRadius(10);
+
+ // Pie chart
+ // dc.pieChart('#statusPieChart')
+ // .radius(180)
+ // .minAngleForLabel(0.2)
+ // .dimension(statusDim)
+ // .group(statusGroup)
+ // .ordinalColors(['lightblue', 'orange', 'green', 'grey'])
+ // .height(295)
+ // .width(500)
+ // .label(d => d.key + ': ' + d.value)
+ // .innerRadius(40)
+
+ // .internalLabels(50)
+ // .drawPaths(true)
+ // .cx(330)
+ // .cy(150)
+ // .legend(
+ // dc
+ // .legend()
+ // .x(30)
+ // .y(65)
+ // .autoItemWidth(true)
+ // .itemHeight(32)
+ // .gap(12)
+ // )
+ // .useViewBoxResizing(true);
}
// Assigned To Row Chart
@@ -192,29 +319,69 @@ function drawUpvotesRowChart(ndx) {
// .renderTitleLabel(true)
// .titleLabelOffsetX(413)
.label(d => d.key + ': ' + d.value)
- .rowsCap(5)
+ .rowsCap(8)
.useViewBoxResizing(true)
+ // .d3.axis.tickFormat()
.dimension(summaryDim)
.group(upvotesGroup);
}
-// Test datatable
-// function drawDataTable(ndx) {
-// let dimension = ndx.dimension(d => d.dim);
-// // let group1 = dimension.groupAll();
-// // let group1 = dimension.group();
-
-// let table1 = dc
-// .dataTable('#datatabletest')
-// // .tableview('#datatabletest')
-// .dimension(dimension)
-// .height(200)
-// .width(200)
-// .size(Infinity)
-// .columns(['summary', 'ticket_type', 'priority', 'status', 'upvotes']);
-// }
-
-// Reset all chart
+function showAgePriorityScatterPlot(ndx) {
+ let dim = ndx.dimension(function(d) {
+ return [d.priority, d.age];
+ });
+ let group = dim.group();
+
+ // print_filter(group);
+
+ dc.bubbleChart('#age-priority-scatter-plot')
+ // dc.scatterPlot('#age-priority-scatter-plot')
+ .width(800)
+ .height(300)
+ .yAxisLabel('Priority')
+ .xAxisLabel('Age')
+ .useViewBoxResizing(true)
+ .x(d3.scaleLinear().domain([0, 30]))
+ // .brushOn(true)
+ .brushOn(false)
+ .clipPadding(30)
+ // .symbolSize(10)
+ // .y(d3.scaleOrdinal().domain(['Low', 'Medium', 'High']))
+ .y(d3.scaleOrdinal().domain(['Low', 'Medium', 'High']))
+ // .y(d3.scaleOrdinal())
+ // Bubble
+ .colors(d3.schemeRdYlGn[9])
+ .colorDomain([0, 30])
+ .colorAccessor(d => d.value)
+ // .colors(d3.scale.category10())
+ .keyAccessor(d => d.key[1])
+ .valueAccessor(d => d.key[0])
+ .radiusValueAccessor(d => d.value)
+ .maxBubbleRelativeSize(0.08)
+ .r(d3.scaleLinear().domain([0, 10]))
+ .title(d => 'Num tix with this priority and age: ' + d.value)
+ .label(d => d.value + ' : ' + d.key)
+ .dimension(dim)
+ .group(group);
+}
+
+function showFilteredCount(ndx) {
+ let all = ndx.groupAll();
+ dc.dataCount('#filtered-count')
+ .crossfilter(ndx)
+ .groupAll(all)
+ .html({
+ some:
+ "
Reset " +
+ '
filter_list ' +
+ '
%filter-count of
%total-count Tickets',
+ all:
+ '
filter_list ' +
+ '
%filter-count of
%total-count Tickets'
+ });
+}
+/*
Reset */
+// Reset all charts
$('.reset').click(function() {
dc.filterAll();
dc.redrawAll();
diff --git a/tickets/forms.py b/tickets/forms.py
index dae0352..c9603d5 100644
--- a/tickets/forms.py
+++ b/tickets/forms.py
@@ -1,21 +1,54 @@
from django import forms
+from django.contrib.auth.models import User
from .models import Ticket, Comment
+# ChoiceField choices
TICKET_TYPES = (('Bug', 'Bug'), ('Feature', 'Feature'))
+PRIORITIES = (('Low', 'Low'), ('Medium', 'Medium'), ('High', 'High'))
+STATUSES = (('New', 'New'), ('In Progress', 'In Progress'),
+ ('Resolved', 'Resolved'), ('Cancelled', 'Cancelled'))
+
+
+# ? not used
+# class DateInput(forms.DateInput):
+# input_type = 'date'
class AddTicketForm(forms.ModelForm):
- ticket_type = forms.ChoiceField(choices=TICKET_TYPES, required=True)
- # tags = forms.CharField(required=False)
+ ticket_type = forms.ChoiceField(
+ choices=TICKET_TYPES, required=True, label='Ticket Type')
+ priority = forms.ChoiceField(choices=PRIORITIES, required=True)
+ status = forms.ChoiceField(choices=STATUSES, required=True)
+
+ class Meta:
+ model = Ticket
+ widgets = {
+ 'summary': forms.TextInput(attrs={'placeholder': 'Summary'}),
+ 'description': forms.Textarea(attrs={'placeholder': 'Add a description',
+ 'rows': 4}),
+ }
+ fields = ('ticket_type', 'summary',
+ 'description', 'priority',
+ 'status', 'tags', 'screenshot')
+
+
+class EditTicketForm(forms.ModelForm):
+ # Same as Add except for AddTicketForm
+ ticket_type = forms.ChoiceField(
+ choices=TICKET_TYPES, required=True, label='Ticket Type')
+ assigned_to = forms.ModelChoiceField(User.objects, label='Assign to')
+ priority = forms.ChoiceField(choices=PRIORITIES, required=True)
+ status = forms.ChoiceField(choices=STATUSES, required=True)
class Meta:
model = Ticket
widgets = {
'summary': forms.TextInput(attrs={'placeholder': 'Summary'}),
- 'description': forms.Textarea(attrs={'placeholder': 'Details', 'rows': 4}),
- # 'tags': forms.TextInput(attrs={'placeholder': 'eg. Project Alpha, Testing'}),
+ 'description': forms.Textarea(attrs={'placeholder': 'Add a description', 'rows': 4}),
}
- fields = ('ticket_type', 'summary', 'description', 'tags')
+ fields = ('ticket_type', 'summary',
+ 'description', 'priority',
+ 'status', 'tags', 'assigned_to', 'screenshot')
class AddCommentForm(forms.ModelForm):
@@ -27,7 +60,6 @@ class Meta:
'comment_body': forms.Textarea(attrs={'placeholder': 'Leave a comment', 'rows': 3})
}
labels = {
- # 'comment_body': ('test'),
'comment_body': '',
}
error_messages = {
diff --git a/tickets/models.py b/tickets/models.py
index 77c8ddf..396d296 100644
--- a/tickets/models.py
+++ b/tickets/models.py
@@ -1,36 +1,57 @@
from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
-from datetime import datetime
from taggit.managers import TaggableManager
from simple_history.models import HistoricalRecords
+import datetime
-# Create your models here.
+# Select Dropdown Options
TICKET_TYPES = (('Bug', 'Bug'), ('Feature', 'Feature'))
+PRIORITIES = (('Low', 'Low'), ('Medium', 'Medium'), ('High', 'High'))
+STATUSES = (('New', 'New'), ('In Progress', 'In Progress'),
+ ('Resolved', 'Resolved'), ('Cancelled', 'Cancelled'))
+
+# Ticket Model
class Ticket(models.Model):
ticket_type = models.CharField(max_length=10, choices=TICKET_TYPES)
summary = models.CharField(max_length=300)
created_date = models.DateTimeField(auto_now_add=True)
- status = models.CharField(max_length=50, default='New')
- priority = models.CharField(max_length=50, default='Medium')
- submitted_by = models.ForeignKey(User, on_delete=models.CASCADE)
- assigned_to = models.CharField(max_length=200, default='Unassigned')
+ resolved_date = models.DateTimeField(null=True, default=None)
+ status = models.CharField(max_length=50, default='New', choices=STATUSES)
+ priority = models.CharField(
+ max_length=50, default='Medium', choices=PRIORITIES)
+ submitted_by = models.ForeignKey(
+ User, on_delete=models.CASCADE, related_name='submitted_by')
+ assigned_to = models.ForeignKey(
+ User, on_delete=models.CASCADE, related_name='assigned_to', default=1)
description = models.TextField()
tags = TaggableManager(blank=True)
upvotes = models.IntegerField(default=0)
+ screenshot = models.ImageField(
+ upload_to='tickets/', null=True, blank=True)
history = HistoricalRecords()
+ def age(self):
+ created_date_only = self.created_date.date()
+ return int((datetime.date.today() - created_date_only).days)
+
+ def days_to_resolve(self):
+ if self.resolved_date:
+ return int((self.resolved_date.date() - self.created_date.date()).days)
+ else:
+ return None
+
def __str__(self):
return self.summary
class Meta:
verbose_name_plural = "Tickets"
- # attachments = models.CharField(maximum_length=200)
+# Comment Model
class Comment(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
@@ -42,10 +63,3 @@ def __str__(self):
class Meta:
verbose_name_plural = "Comments"
-
-
-# class ChangeHistory(models.Model):
-# ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
-# user = models.ForeignKey(User, on_delete=models.CASCADE)
-# date = models.DateTimeField(auto_now_add=True)
- # field?
diff --git a/tickets/serializers.py b/tickets/serializers.py
index 37b9ecc..fd4cc50 100644
--- a/tickets/serializers.py
+++ b/tickets/serializers.py
@@ -4,7 +4,9 @@
class TicketSerializer(serializers.ModelSerializer):
+
class Meta:
model = Ticket
fields = ('ticket_type', 'summary', 'description',
- 'priority', 'assigned_to', 'status', 'upvotes', 'created_date')
+ 'priority', 'assigned_to', 'status', 'upvotes',
+ 'created_date', 'age', 'days_to_resolve')
diff --git a/tickets/templates/add_ticket.html b/tickets/templates/add_ticket.html
index 388297e..9b63463 100644
--- a/tickets/templates/add_ticket.html
+++ b/tickets/templates/add_ticket.html
@@ -1,29 +1,59 @@
{% extends "base.html" %}
{% load static from staticfiles %}
-{% load bootstrap_tags %}
-{% block title %}
-Add Ticket
-{% endblock %}
+{% load crispy_forms_tags %}
+{% block title %}TrackIt | Add Ticket{% endblock %}
{% block head %}
{% endblock %}
{% block page_heading %}
-
Add Ticket
+
add Add Ticket
{% endblock %}
+{% block container-class %}add-ticket-container{% endblock %}
{% block content %}
-
-
-
-
Cancel
+
{% endblock %}
{% block scripts %}
+
{% endblock %}
diff --git a/tickets/templates/dashboard.html b/tickets/templates/dashboard.html
index fe81840..8d7e616 100644
--- a/tickets/templates/dashboard.html
+++ b/tickets/templates/dashboard.html
@@ -33,6 +33,12 @@
+
@@ -71,8 +77,7 @@
In-Demand Tickets
-
-
+
diff --git a/tickets/templates/kanban.html b/tickets/templates/kanban.html
index 07fabf7..5a57db7 100644
--- a/tickets/templates/kanban.html
+++ b/tickets/templates/kanban.html
@@ -3,68 +3,84 @@
{% block title %}TrackIt | Kanban{% endblock %}
{% block container-class %}kanban-container{% endblock %}
{% block page_heading %}
-
view_week Kanban ({{ tickets.count }})
+
view_week Kanban ({{ tickets|length }})
+
+
+
+
+
+
+
{% endblock %}
{% block content %}
-
+
+
-
+
local_activity New
- ({{ new_tickets_count }})
- {% for ticket in tickets %}
- {% if ticket.status == 'New' %}
+ ({{ new_tickets|length }})
+ {% for ticket in new_tickets %}
- {% if ticket.ticket_type == "Bug" %}
- bug_report
- {% elif ticket.ticket_type == "Feature" %}
- build
- {% endif %}
- {{ ticket.summary }}
+ title="{{ ticket.description }}">{{ ticket.summary }}
-
+
+
+ {{ ticket.priority }}
+ badge-success
{% elif ticket.priority == 'Medium' %}
- {{ ticket.priority }}
+ badge-warning
{% elif ticket.priority == 'High' %}
- {{ ticket.priority }}
+ badge-danger
{% endif %}
+ float-right">{{ ticket.priority }}
+
+
+
+ {% if ticket.ticket_type == "Bug" %}
+
bug_report
+ {% elif ticket.ticket_type == "Feature" %}
+
build
+ {% endif %}
+
+ {% if ticket.tags.names %}
- {% if ticket.tags.names %}
-
{% for tag in ticket.tags.names %}
- {{ tag }}
+ {{ tag }}
{% endfor %}
- {% endif %}
+ {% endif %}
+
{% if request.user == ticket.submitted_by or request.user.is_staff %}
-
- {% endif %}
{% endfor %}
+
schedule In Progress
- ({{ in_progress_tickets_count }})
+ ({{ in_progress_tickets|length }})
- {% for ticket in tickets %}
- {% if ticket.status == 'In Progress' %}
+ {% for ticket in in_progress_tickets %}
{{ ticket.summary }}
- {% if ticket.ticket_type == "Bug" %}
- bug_report
- {% elif ticket.ticket_type == "Feature" %}
- build
- {% endif %}
+
+
+
+ {{ ticket.priority }}
+
+
-
- {% if ticket.priority == 'Low' %}
- {{ ticket.priority }}
- {% elif ticket.priority == 'Medium' %}
- {{ ticket.priority }}
- {% elif ticket.priority == 'High' %}
- {{ ticket.priority }}
- {% endif %}
-
+
+
+ {% if ticket.ticket_type == "Bug" %}
+
bug_report
+ {% elif ticket.ticket_type == "Feature" %}
+
build
+ {% endif %}
+
+
+ {% if ticket.tags.names %}
+
+ {% for tag in ticket.tags.names %}
+ {{ tag }}
+ {% endfor %}
+
+ {% endif %}
{% if request.user == ticket.submitted_by or request.user.is_staff %}
-
+ ...
- {% endif %}
{% endfor %}
+
done Resolved
- ({{ resolved_tickets_count }})
+ ({{ resolved_tickets|length }})
- {% for ticket in tickets %}
- {% if ticket.status == 'Resolved' %}
+ {% for ticket in resolved_tickets %}
{{ ticket.summary }}
- {% if ticket.ticket_type == "Bug" %}
- bug_report
- {% elif ticket.ticket_type == "Feature" %}
- build
- {% endif %}
+
+
+
+ {{ ticket.priority }}
+
+
+
-
- {% if ticket.priority == 'Low' %}
- {{ ticket.priority }}
- {% elif ticket.priority == 'Medium' %}
- {{ ticket.priority }}
- {% elif ticket.priority == 'High' %}
- {{ ticket.priority }}
- {% endif %}
-
+
+
+ {% if ticket.ticket_type == "Bug" %}
+
bug_report
+ {% elif ticket.ticket_type == "Feature" %}
+
build
+ {% endif %}
+
+
+ {% if ticket.tags.names %}
+
+ {% for tag in ticket.tags.names %}
+ {{ tag }}
+ {% endfor %}
+
+ {% endif %}
{% if request.user == ticket.submitted_by or request.user.is_staff %}
-
+ ...
- {% endif %}
{% endfor %}
+
cancel Cancelled
- ({{ cancelled_tickets_count }})
+ ({{ cancelled_tickets|length }})
- {% for ticket in tickets %}
- {% if ticket.status == 'Cancelled' %}
+ {% for ticket in cancelled_tickets %}
-
- {% if ticket.priority == 'Low' %}
- {{ ticket.priority }}
- {% elif ticket.priority == 'Medium' %}
- {{ ticket.priority }}
- {% elif ticket.priority == 'High' %}
- {{ ticket.priority }}
- {% endif %}
-
+
+
+ {% if ticket.ticket_type == "Bug" %}
+
bug_report
+ {% elif ticket.ticket_type == "Feature" %}
+
build
+ {% endif %}
+
+
+ {% if ticket.tags.names %}
+
+ {% for tag in ticket.tags.names %}
+ {{ tag }}
+ {% endfor %}
+
+ {% endif %}
{% if request.user == ticket.submitted_by or request.user.is_staff %}
-
+ ...
- {% endif %}
{% endfor %}
diff --git a/tickets/templates/view_ticket.html b/tickets/templates/view_ticket.html
index 38ec04e..eb10a2c 100644
--- a/tickets/templates/view_ticket.html
+++ b/tickets/templates/view_ticket.html
@@ -1,12 +1,12 @@
{% extends "base.html" %}
{% load bootstrap_tags %}
-{% block title %}
-View Ticket (ID: {{ ticket.id }})
-{% endblock %}
-{% block page_heading %}
-View Ticket: ({{ticket.id}})
+{% block title %}TrackIt | View Ticket | {{ ticket.id }}{% endblock %}
+{% block container-class %}view-ticket-container{% endblock %}
+{% block page_heading %}
pageview View Ticket | {{ticket.id}}
+
{% endblock %}
{% block content %}
+
-
-
+
-
-
-
-
{{ ticket.summary}}
- {% if ticket.ticket_type == "Bug" %}
- bug_report
- {% elif ticket.ticket_type == "Feature" %}
- build
+ {% if ticket.status == "New" %}
+
+ {% elif ticket.status == "In Progress" %}
+
+ {% elif ticket.status == "Resolved" %}
+
+ {% elif ticket.status == "Cancelled" %}
+
{% endif %}
-
-
{{ ticket.description }}
-
-
-
-
-
-
-
- Priority |
-
- {% if ticket.priority == "Low" %}
-
- {% elif ticket.priority == "Medium" %}
-
- {% elif ticket.priority == "High" %}
-
- {% endif %}
- {{ ticket.priority }}
-
- |
-
-
- Status |
- {{ ticket.status }} |
-
-
- Submitter |
- {{ ticket.submitted_by }} |
-
-
- Assignee |
- {{ ticket.assigned_to }} |
-
-
- Created |
- {{ ticket.created_date }} |
-
-
- Resolved |
- {% if ticket.est_resolved_date == None %}
- -- |
+
+
+
{{ ticket.summary}}
+ {% if ticket.ticket_type == "Bug" %}
+ bug_report
+ {% elif ticket.ticket_type == "Feature" %}
+ build
+ {% endif %}
+
+
{{ ticket.description }}
+ {% if ticket.tags.names %}
+ {% for tag in ticket.tags.names %}
+
{{ tag }}
+ {% endfor %}
+
+
+ {% endif %}
+
+
+
+
+
+
+
+ Priority |
+
+ {% if ticket.priority == "Low" %}
+
+ {% elif ticket.priority == "Medium" %}
+
+ {% elif ticket.priority == "High" %}
+
+ {% endif %}
+ {{ ticket.priority }}
+
+ |
+
+
+ Status |
+ {% if ticket.status == "New" %}
+ local_activityNew |
+ {% elif ticket.status == "In Progress" %}
+ access_timeIn
+ Progress |
+ {% elif ticket.status == "Resolved" %}
+ doneResolved |
+ {% elif ticket.status == "Cancelled" %}
+ cancelCancelled |
+ {% endif %}
+
+
+ Submitter |
+ {{ ticket.submitted_by }}
+
+ |
+
+
+ Assignee |
+ {{ ticket.assigned_to }}
+
+ |
+
+
+ Created |
+ {{ ticket.created_date }} |
+
+
+ Resolved |
+ {% if ticket.resolved_date == None %}
+ -- |
+ {% else %}
+ {{ ticket.resolved_date }} |
+ {% endif %}
+
+
+ Days to Resolve |
+ {% if ticket.days_to_resolve == None %}
+ -- |
+ {% else %}
+ {{ ticket.days_to_resolve }} |
+ {% endif %}
+
+
+ Screenshot |
+ {% if ticket.screenshot %}
+
+ {{ ticket.screenshot }}
+ |
+
+
+
+ {% else %}
+ -- |
+ {% endif %}
+
+
+
+
+
+
+ {% if ticket.submitted_by == request.user or request.user.is_staff %}
+
+
edit Edit
+
+
+ Update Status
+
+
+
{% else %}
-
{{ ticket.est_resolved_date }} |
+
+
Note: Only the Submitter or Staff can Edit this Ticket
{% endif %}
-
-
- Age (days) |
- {{ ticket.age }} |
-
-
- Tags |
- {{ticket.tags.all|join:", "}} |
-
-
-
+
+
+ {% if all_deltas %}
+
timeline Recent Activity
+
+ {% for delta in all_deltas %}
+ -
+ {{ delta.changed_by }} set {{ delta.field }} to
+ {{ delta.new_value }}
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
-
{{ ticket.upvotes }}
- Upvotearrow_upward
-
Edit
- {% if ticket.status != 'Cancelled' %}
-
Cancel
- {% endif %}
-
Delete
-
-
-
- {% if all_deltas %}
-
timeline Activity
-
- {% for delta in all_deltas %}
- -
-
- {{ delta.changed_by }} set {{ delta.field }} to
- {{ delta.new_value }}
-
- {% endfor %}
-
- {% endif %}
-
-
-
-
- {% if ticket.screenshot %}
-
Screenshot:
-
-
-
- {% endif %}
-
-
-
-
-
-
-
insert_comment Comments ({{ comments.count }})
-
- {% for comment in comments %}
- -
- account_circle
- {{ comment.user }}
- {{ comment.date }}
-
{{ comment.comment_body }}
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
-
{{ ticket.summary}}
- {% if ticket.ticket_type == "Bug" %}
- bug_report
- {% elif ticket.ticket_type == "Feature" %}
- build
- {% endif %}
-
-
{{ ticket.description }}
-
-
-
-
- Property |
- New Value |
- Old Value |
- User |
- Date |
-
-
-
- {% if all_deltas %}
- {% for delta in all_deltas %}
-
-
- {{ delta.field }} |
- {{ delta.new_value }} |
- {{ delta.old_value }} |
- {{ delta.changed_by }} |
- {{ delta.date_changed }} |
-
- {% endfor %}
- {% else %}
-
- - |
- - |
- - |
- - |
- - |
-
- {% endif %}
-
-
+
+
+
insert_comment Comments ({{ comments.count }})
+
+ {% for comment in comments %}
+ -
+ account_circle
+ {{ comment.user }}
+ {{ comment.date }}
+
{{ comment.comment_body }}
+
+ {% endfor %}
+
+
+
+
-
-
-
-
-
-{% endblock %}
+
+
+ {% if ticket.status == "New" %}
+
+ {% elif ticket.status == "In Progress" %}
+
+ {% elif ticket.status == "Resolved" %}
+
+ {% elif ticket.status == "Cancelled" %}
+
+ {% endif %}
+
+
+
{{ ticket.summary}}
+ {% if ticket.ticket_type == "Bug" %}
+ bug_report
+ {% elif ticket.ticket_type == "Feature" %}
+ build
+ {% endif %}
+
+
{{ ticket.description }}
+
+
+
+
+ Property |
+ Old Value |
+ New Value |
+ Changed By |
+ Date Changed |
+
+
+
+ {% if all_deltas %}
+ {% for delta in all_deltas %}
+
+ {{ delta.field }} |
+ {{ delta.old_value }} |
+ {{ delta.new_value }} |
+ {{ delta.changed_by }} |
+ {{ delta.date_changed }} |
+
+ {% endfor %}
+ {% else %}
+
+ - |
+ - |
+ - |
+ - |
+ - |
+
+ {% endif %}
+
+
+
+
+
+
+
+ {% endblock %}
+ {% block scripts %}
+ {% endblock %}
diff --git a/tickets/views.py b/tickets/views.py
index e213862..59fe581 100644
--- a/tickets/views.py
+++ b/tickets/views.py
@@ -132,30 +132,30 @@ def delete_ticket(request, pk=None):
@login_required()
def kanban(request):
"""Show KANBAN View"""
- sort_field = request.GET.get(
- 'sort_by') if 'sort_by' in request.GET else '-id'
+ # my_tickets_only = request.GET.get('my_tickets_only')
+ # if my_tickets_only == 'true':
+ # tickets = Ticket.objects.filter(
+ # Q(assigned_to=request.user.id) | Q(submitted_by=request.user.id))
+ # else:
tickets = Ticket.objects.filter()
- # New tickets count
- new_tickets_count = Ticket.objects.filter(
- status='New').count()
- # In Progress tickets count
- in_progress_tickets_count = Ticket.objects.filter(
- status='In Progress').count()
- # Resolved tickets count
- resolved_tickets_count = Ticket.objects.filter(
- status='Resolved').count()
- # Cancelled tickets count
- cancelled_tickets_count = Ticket.objects.filter(
- status='Cancelled').count()
- # My tickets count
- my_tickets_count = Ticket.objects.filter(
- submitted_by=request.user.id).count()
+
+ new_tickets = tickets.filter(
+ status='New')
+
+ in_progress_tickets = tickets.filter(
+ status='In Progress')
+
+ resolved_tickets = tickets.filter(
+ status='Resolved')
+
+ cancelled_tickets = tickets.filter(
+ status='Cancelled')
+
return render(request, 'kanban.html', {'tickets': tickets,
- 'new_tickets_count': new_tickets_count,
- 'in_progress_tickets_count': in_progress_tickets_count,
- 'resolved_tickets_count': resolved_tickets_count,
- 'cancelled_tickets_count': cancelled_tickets_count,
- 'my_tickets_count': my_tickets_count
+ 'new_tickets': new_tickets,
+ 'in_progress_tickets': in_progress_tickets,
+ 'resolved_tickets': resolved_tickets,
+ 'cancelled_tickets': cancelled_tickets,
})