2020-01-21 06:35:58 +00:00
|
|
|
{% extends "base.html" %}
|
|
|
|
{% block media %}
|
|
|
|
{{ form.media.css }}
|
|
|
|
<style>
|
|
|
|
#content > h2:first-child small {
|
|
|
|
color: #999;
|
|
|
|
font-size: 0.9em;
|
|
|
|
margin-left: 0.3em;
|
|
|
|
}
|
|
|
|
|
|
|
|
#content > h2:first-child .status, #content > h2:first-child .title {
|
|
|
|
display: inline;
|
|
|
|
}
|
|
|
|
|
|
|
|
#content > h2:first-child .fa-check-circle-o {
|
|
|
|
color: #00a900;
|
|
|
|
}
|
|
|
|
|
|
|
|
#content > h2:first-child .fa-exclamation-circle {
|
|
|
|
color: darkred;
|
|
|
|
}
|
|
|
|
|
|
|
|
.container {
|
|
|
|
width: 100%;
|
|
|
|
margin: 0 auto;
|
|
|
|
display: flex;
|
2021-06-10 22:30:37 +00:00
|
|
|
flex-direction: row;
|
|
|
|
flex-wrap: wrap-reverse;
|
2020-01-21 06:35:58 +00:00
|
|
|
max-width: 1000px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ticket-sidebar {
|
|
|
|
flex: 1;
|
|
|
|
padding: 10px 0 0 10px;
|
2021-06-10 22:30:37 +00:00
|
|
|
min-width: 150px;
|
2020-01-21 06:35:58 +00:00
|
|
|
max-width: 200px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ticket-info {
|
|
|
|
position: sticky;
|
|
|
|
top: 60px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ticket-messages {
|
|
|
|
flex: 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
.info-box {
|
|
|
|
margin: 5px 0 10px;
|
|
|
|
border: 1px #999 solid;
|
|
|
|
border-radius: 5px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.info-title {
|
|
|
|
padding: 2px 5px;
|
|
|
|
font-weight: 600;
|
|
|
|
border-bottom: 1px #999 solid;
|
|
|
|
background: #eee;
|
|
|
|
border-radius: 5px 5px 0 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.info-data {
|
|
|
|
padding: 2px 5px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.info-empty {
|
|
|
|
color: #999;
|
|
|
|
font-style: italic;
|
|
|
|
}
|
|
|
|
|
|
|
|
.close-ticket {
|
|
|
|
display: block;
|
|
|
|
width: 100%;
|
|
|
|
background: linear-gradient(to bottom, #4bad00 0%, #278811 100%);
|
|
|
|
border-color: #24710e;
|
|
|
|
font-weight: 600;
|
|
|
|
}
|
|
|
|
|
|
|
|
.close-ticket:hover {
|
|
|
|
background: #24710e;
|
|
|
|
}
|
|
|
|
|
|
|
|
.open-ticket {
|
|
|
|
display: block;
|
|
|
|
width: 100%;
|
|
|
|
background: linear-gradient(to bottom, #ff130f, #b03d17);
|
|
|
|
border-color: #853011;
|
|
|
|
font-weight: 600;
|
|
|
|
}
|
|
|
|
|
|
|
|
.open-ticket:hover {
|
|
|
|
background: #853011;
|
|
|
|
}
|
|
|
|
|
|
|
|
a.edit-notes {
|
|
|
|
float: right;
|
|
|
|
}
|
|
|
|
|
|
|
|
#ticket-notes .info-real :first-child {
|
|
|
|
margin-top: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
#ticket-notes .info-real :last-child {
|
|
|
|
margin-bottom: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.message {
|
|
|
|
margin-top: -40px;
|
|
|
|
padding-top: 55px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.message .info {
|
|
|
|
}
|
|
|
|
|
|
|
|
.message .username {
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.message .gravatar {
|
2021-06-10 22:30:37 +00:00
|
|
|
width: 40px;
|
|
|
|
border-radius: 4px;
|
2020-01-21 06:35:58 +00:00
|
|
|
display: block;
|
|
|
|
margin: 0 auto;
|
|
|
|
}
|
|
|
|
|
|
|
|
.message .message-date, .message .message-time {
|
|
|
|
display: inline-block;
|
|
|
|
}
|
|
|
|
|
|
|
|
.message .detail {
|
|
|
|
border: 1px #999 solid;
|
|
|
|
border-radius: 5px;
|
|
|
|
flex: 1;
|
2021-06-10 22:30:37 +00:00
|
|
|
min-width: 200px;
|
2020-01-21 06:35:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.message .header {
|
|
|
|
background: #eee;
|
|
|
|
color: #777;
|
|
|
|
border-bottom: 1px solid #999;
|
|
|
|
border-radius: 5px 5px 0 0;
|
|
|
|
padding: 2px 7px;
|
2021-06-10 22:30:37 +00:00
|
|
|
display: inline-flex;
|
|
|
|
width: -webkit-fill-available;
|
|
|
|
}
|
|
|
|
|
|
|
|
.message .send-time {
|
2020-01-21 06:35:58 +00:00
|
|
|
text-align: right;
|
|
|
|
}
|
|
|
|
|
|
|
|
.message .content {
|
|
|
|
padding: 7px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.message .content :first-child {
|
|
|
|
margin-top: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.message .content :last-child {
|
|
|
|
margin-bottom: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.new-message .detail {
|
|
|
|
padding: 8px 10px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.new-message .submit, #edit-notes .submit {
|
|
|
|
margin: 10px 0 0 auto;
|
|
|
|
}
|
2021-06-10 22:30:37 +00:00
|
|
|
|
|
|
|
.user-container {
|
|
|
|
display: inline-flex;
|
|
|
|
}
|
|
|
|
|
|
|
|
.user-container .username {
|
|
|
|
padding-left: 0.5em;
|
|
|
|
padding-top: 1.65em;
|
|
|
|
}
|
2020-01-21 06:35:58 +00:00
|
|
|
</style>
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
{% block js_media %}
|
|
|
|
{{ form.media.js }}
|
|
|
|
<script src="{{ static('libs/featherlight/featherlight.min.js') }}" type="text/javascript"></script>
|
|
|
|
<script type="text/javascript" src="{{ static('event.js') }}"></script>
|
|
|
|
<script type="text/javascript">
|
|
|
|
$(function () {
|
|
|
|
var $h2 = $('#content').find('> h2:first-child');
|
|
|
|
var $status = $h2.find('.status i'), $title = $h2.find('.title');
|
|
|
|
|
|
|
|
function update_ticket_state(open) {
|
|
|
|
if (open)
|
|
|
|
$status.removeClass('fa-check-circle-o').addClass('fa-exclamation-circle');
|
|
|
|
else
|
|
|
|
$status.removeClass('fa-exclamation-circle').addClass('fa-check-circle-o');
|
|
|
|
$('.close-ticket').toggle(open);
|
|
|
|
$('.open-ticket').toggle(!open);
|
|
|
|
}
|
|
|
|
|
|
|
|
$('.close-ticket, .open-ticket').click(function () {
|
|
|
|
var open = $(this).attr('data-open') == '1';
|
|
|
|
$.ajax({
|
|
|
|
url: $(this).attr('data-ajax'), type: 'POST',
|
|
|
|
success: function () {
|
|
|
|
update_ticket_state(open);
|
|
|
|
},
|
|
|
|
error: function (data) {
|
|
|
|
alert('Could not change ticket: ' + data.responseText);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
$('.edit-notes').featherlight({
|
|
|
|
afterOpen: function () {
|
|
|
|
var $form = $('#edit-notes');
|
|
|
|
$form.find('.submit').click(function () {
|
|
|
|
$.post($form.attr('action'), $form.serialize()).done(function (data) {
|
|
|
|
$('#ticket-notes').find('.info-empty').toggle(!data).end().find('.info-real').html(data);
|
|
|
|
$.featherlight.current().close();
|
|
|
|
});
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var page_ref_key = 'ticket:open:{{ ticket.id }}', page_close_key = page_ref_key + ':close';
|
|
|
|
var page_ref;
|
|
|
|
|
|
|
|
function increase_page_ref() {
|
|
|
|
if (page_ref_key in localStorage)
|
|
|
|
localStorage[page_ref_key] = page_ref = +localStorage[page_ref_key] + 1;
|
|
|
|
else
|
|
|
|
localStorage[page_ref_key] = page_ref = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
function decrease_page_ref() {
|
|
|
|
if (page_ref_key in localStorage) {
|
|
|
|
localStorage[page_close_key] = page_ref;
|
|
|
|
delete localStorage[page_close_key];
|
|
|
|
localStorage[page_ref_key] = +localStorage[page_ref_key] - 1;
|
|
|
|
}
|
|
|
|
page_ref = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function is_highest_ref() {
|
|
|
|
console.log(localStorage[page_ref_key], page_ref);
|
|
|
|
if (page_ref_key in localStorage)
|
|
|
|
return +localStorage[page_ref_key] == page_ref;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$(window).on('storage', function (e) {
|
|
|
|
e = e.originalEvent;
|
|
|
|
if (e.key == page_close_key && e.newValue !== null)
|
|
|
|
if (page_ref != null && page_ref > +e.newValue)
|
|
|
|
--page_ref;
|
|
|
|
});
|
|
|
|
|
|
|
|
register_notify('ticket', {
|
|
|
|
change: function (enabled) {
|
|
|
|
if (enabled)
|
|
|
|
increase_page_ref();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
$(window).on('beforeunload', function () {
|
|
|
|
decrease_page_ref();
|
|
|
|
});
|
|
|
|
|
|
|
|
function ticket_status(ticket) {
|
|
|
|
update_ticket_state(ticket.open);
|
|
|
|
if (is_highest_ref())
|
|
|
|
notify('ticket', (ticket.open ? '{{ _('Reopened: ') }}' :
|
|
|
|
'{{ _('Closed: ') }}') + $title.text());
|
|
|
|
}
|
|
|
|
|
|
|
|
function ticket_message(ticket) {
|
|
|
|
$.ajax({
|
|
|
|
url: '{{ url('ticket_message_ajax', ticket.id) }}',
|
|
|
|
data: {message: ticket.message},
|
|
|
|
success: function (data) {
|
|
|
|
$('#messages').append($(data.message));
|
|
|
|
},
|
|
|
|
error: function (data) {
|
|
|
|
if (data.status === 403)
|
|
|
|
console.log('No right to see: ' + ticket.message);
|
|
|
|
else {
|
|
|
|
console.log('Could not load ticket message:');
|
|
|
|
console.log(data.responseText);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
window.load_dynamic_update = function (last_msg) {
|
|
|
|
return new EventReceiver(
|
|
|
|
"{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}",
|
|
|
|
['ticket-{{ ticket.id }}'], last_msg, function (message) {
|
|
|
|
switch (message.type) {
|
|
|
|
case 'ticket-status':
|
|
|
|
ticket_status(message);
|
|
|
|
break;
|
|
|
|
case 'ticket-message':
|
|
|
|
ticket_message(message);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
|
|
|
|
{% if last_msg %}
|
|
|
|
<script type="text/javascript">
|
|
|
|
$(function () {
|
|
|
|
load_dynamic_update({{last_msg}});
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
{% endif %}
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
{% block content_title %}
|
|
|
|
<div class="status">
|
|
|
|
<i class="fa {% if ticket.is_open %}fa-exclamation-circle{% else %}fa-check-circle-o{% endif %}"></i>
|
|
|
|
</div>
|
|
|
|
<div class="title">{{ ticket.title }}</div><small>#{{ ticket.id }}</small>
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
{% block body %}
|
|
|
|
<div class="container">
|
2021-06-10 22:30:37 +00:00
|
|
|
<div class="ticket-messages">
|
|
|
|
<main id="messages" class="messages">
|
|
|
|
{% for message in ticket_messages %}
|
|
|
|
{% include "ticket/message.html" %}
|
|
|
|
{% endfor %}
|
|
|
|
</main>
|
|
|
|
<hr>
|
|
|
|
<section class="message new-message">
|
|
|
|
<div class="info">
|
|
|
|
<a href="{{ url('user_page', request.user.username) }}" class="user">
|
|
|
|
<img src="{{ gravatar(request.user, 135) }}" class="gravatar">
|
|
|
|
<div class="username {{ request.profile.css_class }}">{{ request.user.username }}</div>
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
<div class="detail">
|
|
|
|
<form action="" method="post">
|
|
|
|
{% csrf_token %}
|
|
|
|
{% if form.non_field_errors() or form.body.errors %}
|
|
|
|
<div class="form-errors">
|
|
|
|
{{ form.non_field_errors() }}
|
|
|
|
{{ form.body.errors }}
|
|
|
|
</div>
|
|
|
|
{% endif %}
|
|
|
|
<div class="body-block">{{ form.body }}</div>
|
|
|
|
<button type="submit" class="submit">{{ _('Post') }}</button>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</div>
|
2020-01-21 06:35:58 +00:00
|
|
|
<aside class="ticket-sidebar">
|
|
|
|
<div class="ticket-info">
|
|
|
|
<div class="info-box">
|
|
|
|
<div class="info-title">{{ _('Associated object') }}</div>
|
|
|
|
<div class="info-data">
|
|
|
|
<a href="{{ ticket.linked_item.get_absolute_url() }}">{{ ticket.linked_item }}</a>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="info-box">
|
|
|
|
<div class="info-title">{{ _('Assignees') }}</div>
|
|
|
|
<div class="info-data">
|
|
|
|
{% if assignees %}
|
|
|
|
{{ link_users(assignees) }}
|
|
|
|
{% else %}
|
|
|
|
<div class="info-empty">{{ _('No one is assigned.') }}</div>
|
|
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<button data-ajax="{{ url('ticket_close', ticket.id) }}" data-open="0" class="close-ticket"
|
|
|
|
{% if not ticket.is_open %}style="display: none"{% endif %}>{{ _('Close ticket') }}</button>
|
|
|
|
<button data-ajax="{{ url('ticket_open', ticket.id) }}" data-open="1" class="open-ticket"
|
|
|
|
{% if ticket.is_open %}style="display: none"{% endif %}>{{ _('Reopen ticket') }}</button>
|
|
|
|
|
|
|
|
{% if perms.judge.change_ticket or request.profile in assignees %}
|
|
|
|
<div class="info-box">
|
|
|
|
<div class="info-title">{{ _('Assignee notes') }}
|
|
|
|
<a href="#" data-featherlight="{{ url('ticket_notes', ticket.id) }}" class="edit-notes">
|
|
|
|
<i class="fa fa-pencil"></i>
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
<div id="ticket-notes" class="info-data">
|
|
|
|
<div{% if ticket.notes %} style="display: none"{% endif %} class="info-empty">
|
|
|
|
{{ _('Nothing here.') }}
|
|
|
|
</div>
|
|
|
|
<div class="info-real">{{ ticket.notes|linebreaks }}</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
</aside>
|
|
|
|
</div>
|
|
|
|
{% endblock %}
|
2020-06-30 16:42:45 +00:00
|
|
|
|
|
|
|
{% block bodyend %}
|
|
|
|
{{ super() }}
|
|
|
|
{% if REQUIRE_JAX %}
|
|
|
|
{% include "mathjax-load.html" %}
|
|
|
|
{% endif %}
|
|
|
|
{% include "comments/math.html" %}
|
|
|
|
{% endblock %}
|