diff --git a/accounts/templates/index.html b/accounts/templates/index.html index 370baa9..f670855 100644 --- a/accounts/templates/index.html +++ b/accounts/templates/index.html @@ -32,6 +32,9 @@

Issue and Feature tracking done right


Logged in as: {{ user.username }}

My Account

+ {% if user.is_authenticated and not user.profile.is_pro_user %} +

Go PRO

+ {% endif %} {% endif %} @@ -93,7 +96,8 @@
Nice

-

verified_user See Our Customers

+

verified_user See Our + Customers


diff --git a/accounts/templates/profile.html b/accounts/templates/profile.html index e376ede..031f13f 100644 --- a/accounts/templates/profile.html +++ b/accounts/templates/profile.html @@ -1,10 +1,12 @@ {% extends 'base.html' %} -{% block title %}{{ user }}'s Profile{% endblock %} -{% block page_heading %} account_circle My Account{% endblock %} +{% block title %}TrackIt | Account{% endblock %} +{% block page_heading %}account_circle Account +
+{% endblock %} +{% block container-class %}profile-container{% endblock %} {% block content %}
-

account_circle

Add Avatar

@@ -25,7 +27,10 @@ Email - {{ user.email }} Update + + {{ user.email }} + + Member Since @@ -40,45 +45,12 @@ {% if user.profile.is_pro_user %} PRO {% else %} - Basic + Basic Go PRO + {% endif %}
-
-
-

Plans

-
-
-
-
-
-
- {% if user.profile.is_pro_user %} -
BASIC
-

Max Tickets/Month: 10/month

- {% else %} -
BASIC Current
-

Max Tickets/Month: 10/month

-

Tickets Remaining This Month: placeholder

- {% endif %} -
-
-
-
- {% if user.profile.is_pro_user %} -
PRO Current Plan
-

Max Tickets/Month: Unlimited

-

Kanban View: Available

- {% else %} -
PRO
- Unlock Kanban View

-

9.90 EUR / month

-

Go PRO

- {% endif %} -
-
-
{% endblock %} diff --git a/accounts/templates/user_list.html b/accounts/templates/user_list.html index 73141a3..aebd0b1 100644 --- a/accounts/templates/user_list.html +++ b/accounts/templates/user_list.html @@ -1,50 +1,95 @@ {% extends 'base.html' %} -{% block title %}User List{% endblock %} -{% block page_heading %}people User List{% endblock %} +{% block title %}Trackit | Team{% endblock %} +{% block page_heading %}people Users ({{ users.count }}) +
+{% endblock %} {% block head %} - {% endblock %} +{% block container-class %}user-list-container{% endblock %} {% block content %} -
-
- - - - - - - - - - - - - - {% for user in users %} - - - - - - - - - - {% endfor %} - -
IDUserFirst NameLast NameRoleSuperUserEmail
{{ user.id }}{{ user.username }}{{ user.first_name }}{{ user.last_name }}placeholder{{ user.is_superuser }}{{ user.email }}
+ + + + + {% 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 %} -
-
-
- {% csrf_token %} - {{ form | as_bootstrap }} - -
- Cancel +
+
+
+
+ {% csrf_token %} +
+
+ {{ form.summary | as_crispy_field }} +
+
+ {{ form.description | as_crispy_field }} +
+
+ {{ form.ticket_type | as_crispy_field }} +
+
+ {{ form.priority | as_crispy_field }} +
+
+ {{ form.status | as_crispy_field }} +
+
+
+
+ {{ form.tags | as_crispy_field }} +
+
+
+
+ {{ form.screenshot | as_crispy_field }} +
+
+
+ + 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 @@