Commit 7ed998e9 authored by Mirko Vucicevich's avatar Mirko Vucicevich
Browse files

Added SLAs

parent e5134913
......@@ -46,6 +46,15 @@ class NoticeInline(admin.StackedInline):
extra = 1
class SLAInline(admin.StackedInline):
model = ServiceLevelAgreement
fields = [
'body'
]
class ProjectAdmin(MarkdownxModelAdmin):
search_fields = ['title']
list_filter = ['groups__name']
......@@ -65,10 +74,30 @@ class ProjectAdmin(MarkdownxModelAdmin):
prepopulated_fields = {'slug': ("title",)}
filter_horizontal = ['technologies', 'groups']
inlines = [ContributorInline, ContactInline, NoticeInline]
inlines = [ContributorInline, ContactInline, NoticeInline, SLAInline]
def get_groups(self, obj):
return ", ".join(obj.groups.order_by('name').values_list('name', flat=True))
def save_formset(self, request, form, formset, change):
# Create revisions for SLA versions...
if formset.model == ServiceLevelAgreement:
instances = formset.save(commit=False)
for instance in instances:
if instance.id is not None:
# Clone this guy...
original = ServiceLevelAgreement.objects\
.values('author_id', 'body', 'date_updated')\
.get(id=instance.id)
print(original)
SLAVersion.objects.create(
sla=instance,
**original
)
instance.author = request.user
instance.save()
else:
formset.save()
class CustomUserAdmin(UserAdmin):
fieldsets = (
......
# Generated by Django 2.2.16 on 2020-10-07 18:04
from django.db import migrations, models
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('projector', '0032_auto_20201007_1338'),
]
operations = [
migrations.AlterField(
model_name='project',
name='description',
field=markdownx.models.MarkdownxField(help_text='Markdown-formatted project description'),
),
migrations.AlterField(
model_name='project',
name='slug',
field=models.SlugField(help_text='Must be unique. Used for URLs', max_length=250, unique=True),
),
]
# Generated by Django 2.2.16 on 2020-10-07 18:37
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('projector', '0033_auto_20201007_1404'),
]
operations = [
migrations.CreateModel(
name='ServiceLevelAgreement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_updated', models.DateTimeField(auto_now=True)),
('body', markdownx.models.MarkdownxField()),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='projector.Project')),
],
),
migrations.CreateModel(
name='SLA_versions',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_updated', models.DateTimeField()),
('body', models.TextField()),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('sla', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='past_versions', to='projector.ServiceLevelAgreement')),
],
),
]
# Generated by Django 2.2.16 on 2020-10-07 19:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('projector', '0034_servicelevelagreement_sla_versions'),
]
operations = [
migrations.RenameModel(
old_name='SLA_versions',
new_name='SLAVersion',
),
]
......@@ -2,6 +2,8 @@ from .projects import (
Project,
Technology,
Notice,
ServiceLevelAgreement,
SLAVersion,
)
from .users import (
......
......@@ -139,6 +139,40 @@ class Technology(models.Model):
return self.name
class ServiceLevelAgreement(models.Model):
project = models.OneToOneField(
'projector.Project',
on_delete=models.CASCADE
)
author = models.ForeignKey(
'projector.User',
null=True, blank=True,
on_delete=models.SET_NULL
)
date_updated = models.DateTimeField(auto_now=True)
body = MarkdownxField()
class SLAVersion(models.Model):
# Identical to above -- we store a version of past SLAs when they change
# Business logic found in admin.py
sla = models.ForeignKey(
'projector.ServiceLevelAgreement',
on_delete=models.CASCADE,
related_name='past_versions'
)
author = models.ForeignKey(
'projector.User',
null=True, blank=True,
on_delete=models.SET_NULL
)
date_updated = models.DateTimeField()
body = models.TextField()
class Meta:
ordering = ('-date_updated',)
......
@media print {
body{
font-size: 11pt;
}
a{
color: inherit;
}
a:not(href^="#")::after{
content: " [" attr(href) "]";
}
.view-this{
display: none;
}
}
body{
max-width: 800px;
margin: 0 auto;
font-size: 12pt;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
}
h1{
font-size: 1.5em;
}
h2{
font-size: 1.3em;
}
h3{
font-size: 1.13em;
}
h4{
font-size: 1.1em;
}
table{
font-size: 11pt;
width: 50%;
}
th{
text-align: left;
}
a{
text-decoration: none;
}
.toc ul{
padding-inline-start: 20px;
}
.toc>ul{
list-style-type: decimal;
}
.toc>ul>li>ul{
list-style-type: lower-alpha;
}
.toc>ul>li>ul>li>ul{
list-style-type: lower-roman;
}
\ No newline at end of file
......@@ -35,6 +35,11 @@
<a class="nav-link" id="pills-profile-tab" data-toggle="pill" href="#pills-profile" role="tab" aria-controls="pills-profile" aria-selected="false">Notifications <span class="badge badge-light">{{ project.active_notices.count }}</span></a>
</li>
{% endif %}
{% if project.servicelevelagreement %}
<li class="nav-item">
<a class="nav-link" id="pills-sla-tab" data-toggle="pill" href="#pills-sla" role="tab" aria-controls="pills-profile" aria-selected="false">Service Level Agreement</a>
</li>
{% endif %}
{% if project.gitlab_id and project.changelogmd %}
<li class="nav-item">
<a class="nav-link" id="pills-changelog-tab" data-toggle="pill" href="#pills-changelog" role="tab" aria-controls="pills-changelog" aria-selected="false">Change Log</a>
......@@ -43,7 +48,7 @@
</ul>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade show active" id="pills-home" role="tabpanel" aria-labelledby="pills-home-tab">
{{ project.html|safe }}
{{ project.description|markdown }}
</div>
<div class="tab-pane fade" id="pills-profile" role="tabpanel" aria-labelledby="pills-profile-tab">
{% for notice in project.active_notices %}
......@@ -56,8 +61,19 @@
</div>
{% endfor %}
</div>
{% if project.servicelevelagreement %}
<div class="tab-pane fade" id="pills-sla" role="tabpanel" aria-labelledby="pills-sla-tab">
<div class="d-flex justify-content-between align-items-end">
<span>Updated on {{project.servicelevelagreement.date_updated}}</span>
<a href="{% url 'service-level-agreement' project.slug %}" target="_blank" class="">
View Printable Version / History
</a>
</div>
{{ project.servicelevelagreement.body|markdown }}
</div>
{% endif %}
<div class="tab-pane fade" id="pills-changelog" role="tabpanel" aria-labelledby="pills-changelog-tab">
{{ project.changelogmd|safe }}
{{ project.changelog|markdown }}
</div>
<div class="tab-pane fade" id="pills-contact" role="tabpanel" aria-labelledby="pills-contact-tab">
<div class="card my-3">
......
{% load components %}
{% load static %}
<head>
<link rel="stylesheet" href="{% static 'printable.css' %}">
<style>
.d-none{
display: none;
}
</style>
</head>
<h1>
{{project.title}} Service Level Agreement
</h1>
<table class="table table-sm table-bordered">
<thead>
<tr><th>Revision Date</th><th>Author</th><th></th></tr>
</thead>
<tbody>
<tr>
{% with project.servicelevelagreement as sla %}
<td>{{sla.date_updated}}</td>
<td>{{sla.author}}</td>
<td class="sla-version text-center">
<span class="currently-viewing d-none badge badge-pill badge-success">Viewing</span>
<a href="#" class="view-this d-none badge badge-pill badge-primary" onclick="view('current')">View</a>
</td>
{% endwith %}
</tr>
{% for sla in project.servicelevelagreement.past_versions.all %}
<tr>
<td>{{sla.date_updated}}</td>
<td>{{sla.author}}</td>
<td class="sla-version text-center">
<span class="currently-viewing d-none badge badge-pill badge-success">Viewing</span>
<a href="#" class="view-this d-none badge badge-pill badge-primary" onclick="view('{{sla.id}}')">View</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h1>Table of Contents</h1>
{% with project.servicelevelagreement as sla %}
<div class="sla-view d-none" id="current">
{{sla.body|markdown_with_toc}}
</div>
{% endwith %}
{% for sla in project.servicelevelagreement.past_versions.all %}
<div class="sla-view d-none" id="{{sla.id}}">
{{sla.body|markdown_with_toc}}
</div>
{% endfor %}
<script>
const searchParams = new URLSearchParams(window.location.search);
let currently_viewing = searchParams.get('view') || 'current'
view(currently_viewing, true)
function updateURL(val){
searchParams.set('view', val)
let newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + searchParams.toString();
window.history.pushState({path: newurl}, '', newurl)
}
function view(val, squash_url_change){
let views = document.querySelectorAll('.sla-view')
let links = document.querySelectorAll('.sla-version')
for (let i=0; i < views.length; i++){
let content = views[i]
let link = links[i]
let id = content.getAttribute('id')
if ( id == val){
content.classList.remove('d-none')
link.querySelector('.currently-viewing').classList.remove('d-none')
link.querySelector('.view-this').classList.add('d-none')
}
else {
content.classList.add('d-none')
link.querySelector('.currently-viewing').classList.add('d-none')
link.querySelector('.view-this').classList.remove('d-none')
}
}
if (!squash_url_change){
updateURL(val)
}
}
</script>
\ No newline at end of file
......@@ -12,6 +12,8 @@
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'style.css' %}" />
{% block head %}
{% endblock %}
</head>
<body class="main d-flex flex-column h-100">
<!-- Begin page content -->
......
from django import template
import urllib, hashlib
import markdown
from markdown.extensions.toc import TocExtension
from django.utils.safestring import mark_safe
register = template.Library()
......@@ -38,3 +41,14 @@ def user_avatar(user, size='125px'):
'avatar_url': avatar_url,
'size': size
}
@register.filter(name='markdown', is_safe=True)
def to_markdown(value):
return mark_safe(markdown.markdown(value))
@register.filter(name='markdown_with_toc', is_safe=True)
def to_markdown_with_toc(value):
if not '[TOC]\n\n' in value:
value = '[TOC]\n\n' + value
return mark_safe(markdown.markdown(value, extensions=[TocExtension()]))
\ No newline at end of file
......@@ -36,8 +36,11 @@ urlpatterns = [
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
path('<int:pk>/json', project_json_view, name='json-detail'),
path('<int:pk>/send-message', send_message, name='send-message'),
# NOTE Should put most new urls in here
path('<slug:slug>/', views.DetailView.as_view(), name='detail'),
path('<slug:slug>/json', project_json_view, name='json-detail'),
path('<slug:slug>/send-message', send_message, name='send-message'),
path('<slug:slug>/sla', views.ServiceLevelAgreement.as_view(), name='service-level-agreement'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
......@@ -70,6 +70,9 @@ class DetailView(generic.DetailView):
context['contributors'] = contributors
return context
class ServiceLevelAgreement(generic.DetailView):
queryset = Project.objects.exclude(servicelevelagreement__isnull=True)
template_name = 'service-level-agreement.html'
def about(request):
return render(request, 'about.html')
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment