import json
from itertools import chain
from django import forms
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ImproperlyConfigured, PermissionDenied, ValidationError
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404
from django.template.defaultfilters import truncatechars
from django.template.loader import get_template
from django.urls import reverse, reverse_lazy
from django.utils.functional import cached_property
from django.utils.html import escape, format_html, linebreaks
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy
from django.views import View
from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from judge import event_poster as event
from judge.models import Problem, Profile, Ticket, TicketMessage, Notification
from judge.utils.diggpaginator import DiggPaginator
from judge.utils.tickets import filter_visible_tickets, own_ticket_filter
from judge.utils.views import SingleObjectFormView, TitleMixin, paginate_query_context
from judge.views.problem import ProblemMixin
from judge.widgets import HeavyPreviewPageDownWidget
ticket_widget = (forms.Textarea() if HeavyPreviewPageDownWidget is None else
HeavyPreviewPageDownWidget(preview=reverse_lazy('ticket_preview'),
preview_timeout=1000, hide_preview_button=True))
def add_ticket_notifications(users, author, link, ticket):
html = f"{ticket.linked_item}"
users = set(users)
if author in users:
users.remove(author)
for user in users:
notification = Notification(owner=user,
html_link=html,
category='Ticket',
author=author)
notification.save()
class TicketForm(forms.Form):
title = forms.CharField(max_length=100, label=gettext_lazy('Ticket title'))
body = forms.CharField(widget=ticket_widget)
def __init__(self, request, *args, **kwargs):
self.request = request
super(TicketForm, self).__init__(*args, **kwargs)
self.fields['title'].widget.attrs.update({'placeholder': _('Ticket title')})
self.fields['body'].widget.attrs.update({'placeholder': _('Issue description')})
def clean(self):
if self.request is not None and self.request.user.is_authenticated:
profile = self.request.profile
if profile.mute:
raise ValidationError(_('Your part is silent, little toad.'))
return super(TicketForm, self).clean()
class NewTicketView(LoginRequiredMixin, SingleObjectFormView):
form_class = TicketForm
template_name = 'ticket/new.html'
def get_assignees(self):
return []
def get_form_kwargs(self):
kwargs = super(NewTicketView, self).get_form_kwargs()
kwargs['request'] = self.request
return kwargs
def form_valid(self, form):
ticket = Ticket(user=self.request.profile, title=form.cleaned_data['title'])
ticket.linked_item = self.object
ticket.save()
message = TicketMessage(ticket=ticket, user=ticket.user, body=form.cleaned_data['body'])
message.save()
ticket.assignees.set(self.get_assignees())
link = reverse('ticket', args=[ticket.id])
add_ticket_notifications(ticket.assignees.all(), ticket.user, link, ticket)
if event.real:
event.post('tickets', {
'type': 'new-ticket', 'id': ticket.id,
'message': message.id, 'user': ticket.user_id,
'assignees': list(ticket.assignees.values_list('id', flat=True)),
})
return HttpResponseRedirect(link)
class NewProblemTicketView(ProblemMixin, TitleMixin, NewTicketView):
template_name = 'ticket/new_problem.html'
def get_assignees(self):
return self.object.authors.all()
def get_title(self):
return _('New ticket for %s') % self.object.name
def get_content_title(self):
return mark_safe(escape(_('New ticket for %s')) %
format_html('{1}', reverse('problem_detail', args=[self.object.code]),
self.object.translated_name(self.request.LANGUAGE_CODE)))
def form_valid(self, form):
if not self.object.is_accessible_by(self.request.user):
raise Http404()
return super().form_valid(form)
class TicketCommentForm(forms.Form):
body = forms.CharField(widget=ticket_widget)
class TicketMixin(object):
model = Ticket
def get_object(self, queryset=None):
ticket = super(TicketMixin, self).get_object(queryset)
profile_id = self.request.profile.id
if self.request.user.has_perm('judge.change_ticket'):
return ticket
if ticket.user_id == profile_id:
return ticket
if ticket.assignees.filter(id=profile_id).exists():
return ticket
linked = ticket.linked_item
if isinstance(linked, Problem) and linked.is_editable_by(self.request.user):
return ticket
raise PermissionDenied()
class TicketView(TitleMixin, LoginRequiredMixin, TicketMixin, SingleObjectFormView):
form_class = TicketCommentForm
template_name = 'ticket/ticket.html'
context_object_name = 'ticket'
def form_valid(self, form):
message = TicketMessage(user=self.request.profile,
body=form.cleaned_data['body'],
ticket=self.object)
message.save()
link = '%s#message-%d' % (reverse('ticket', args=[self.object.id]), message.id)
notify_list = list(chain(self.object.assignees.all(), [self.object.user]))
add_ticket_notifications(notify_list, message.user, link, self.object)
if event.real:
event.post('tickets', {
'type': 'ticket-message', 'id': self.object.id,
'message': message.id, 'user': self.object.user_id,
'assignees': list(self.object.assignees.values_list('id', flat=True)),
})
event.post('ticket-%d' % self.object.id, {
'type': 'ticket-message', 'message': message.id,
})
return HttpResponseRedirect(link)
def get_title(self):
return _('%(title)s - Ticket %(id)d') % {'title': self.object.title, 'id': self.object.id}
def get_context_data(self, **kwargs):
context = super(TicketView, self).get_context_data(**kwargs)
context['ticket_messages'] = self.object.messages.select_related('user__user')
context['assignees'] = self.object.assignees.select_related('user')
context['last_msg'] = event.last()
return context
class TicketStatusChangeView(LoginRequiredMixin, TicketMixin, SingleObjectMixin, View):
open = None
def post(self, request, *args, **kwargs):
if self.open is None:
raise ImproperlyConfigured('Need to define open')
ticket = self.get_object()
if ticket.is_open != self.open:
ticket.is_open = self.open
ticket.save()
if event.real:
event.post('tickets', {
'type': 'ticket-status', 'id': ticket.id,
'open': self.open, 'user': ticket.user_id,
'assignees': list(ticket.assignees.values_list('id', flat=True)),
'title': ticket.title,
})
event.post('ticket-%d' % ticket.id, {
'type': 'ticket-status', 'open': self.open,
})
return HttpResponse(status=204)
class TicketNotesForm(forms.Form):
notes = forms.CharField(widget=forms.Textarea(), required=False)
class TicketNotesEditView(LoginRequiredMixin, TicketMixin, SingleObjectFormView):
template_name = 'ticket/edit-notes.html'
form_class = TicketNotesForm
context_object_name = 'ticket'
def get_initial(self):
return {'notes': self.get_object().notes}
def form_valid(self, form):
ticket = self.get_object()
ticket.notes = notes = form.cleaned_data['notes']
ticket.save()
if notes:
return HttpResponse(linebreaks(notes, autoescape=True))
else:
return HttpResponse()
def form_invalid(self, form):
return HttpResponseBadRequest()
class TicketList(LoginRequiredMixin, ListView):
model = Ticket
template_name = 'ticket/list.html'
context_object_name = 'tickets'
paginate_by = 50
paginator_class = DiggPaginator
@cached_property
def user(self):
return self.request.user
@cached_property
def profile(self):
return self.user.profile
@cached_property
def can_edit_all(self):
return self.request.user.has_perm('judge.change_ticket')
@cached_property
def filter_users(self):
return self.request.GET.getlist('user')
@cached_property
def filter_assignees(self):
return self.request.GET.getlist('assignee')
def GET_with_session(self, key):
if not self.request.GET:
return self.request.session.get(key, False)
return self.request.GET.get(key, None) == '1'
def _get_queryset(self):
return Ticket.objects.select_related('user__user').prefetch_related('assignees__user').order_by('-id')
def get_queryset(self):
queryset = self._get_queryset()
if self.GET_with_session('own'):
queryset = queryset.filter(own_ticket_filter(self.profile.id))
elif not self.can_edit_all:
queryset = filter_visible_tickets(queryset, self.user, self.profile)
if self.filter_assignees:
queryset = queryset.filter(assignees__user__username__in=self.filter_assignees)
if self.filter_users:
queryset = queryset.filter(user__user__username__in=self.filter_users)
return queryset.distinct()
def get_context_data(self, **kwargs):
context = super(TicketList, self).get_context_data(**kwargs)
page = context['page_obj']
context['title'] = _('Tickets - Page %(number)d of %(total)d') % {
'number': page.number,
'total': page.paginator.num_pages,
}
context['can_edit_all'] = self.can_edit_all
context['filter_status'] = {
'own': self.GET_with_session('own'), 'user': self.filter_users, 'assignee': self.filter_assignees,
'user_id': json.dumps(list(Profile.objects.filter(user__username__in=self.filter_users)
.values_list('id', flat=True))),
'assignee_id': json.dumps(list(Profile.objects.filter(user__username__in=self.filter_assignees)
.values_list('id', flat=True))),
'own_id': self.profile.id if self.GET_with_session('own') else 'null',
}
context['last_msg'] = event.last()
context.update(paginate_query_context(self.request))
return context
def post(self, request, *args, **kwargs):
to_update = ('own',)
for key in to_update:
if key in request.GET:
val = request.GET.get(key) == '1'
request.session[key] = val
else:
request.session.pop(key, None)
return HttpResponseRedirect(request.get_full_path())
class ProblemTicketListView(TicketList):
def _get_queryset(self):
problem = get_object_or_404(Problem, code=self.kwargs.get('problem'))
if problem.is_editable_by(self.request.user):
return problem.tickets.order_by('-id')
elif problem.is_accessible_by(self.request.user):
return problem.tickets.filter(own_ticket_filter(self.profile.id)).order_by('-id')
raise Http404()
class TicketListDataAjax(TicketMixin, SingleObjectMixin, View):
def get(self, request, *args, **kwargs):
try:
self.kwargs['pk'] = request.GET['id']
except KeyError:
return HttpResponseBadRequest()
ticket = self.get_object()
message = ticket.messages.first()
return JsonResponse({
'row': get_template('ticket/row.html').render({'ticket': ticket}, request),
'notification': {
'title': _('New Ticket: %s') % ticket.title,
'body': '%s\n%s' % (_('#%(id)d, assigned to: %(users)s') % {
'id': ticket.id,
'users': (_(', ').join(ticket.assignees.values_list('user__username', flat=True)) or _('no one')),
}, truncatechars(message.body, 200)),
},
})
class TicketMessageDataAjax(TicketMixin, SingleObjectMixin, View):
def get(self, request, *args, **kwargs):
try:
message_id = request.GET['message']
except KeyError:
return HttpResponseBadRequest()
ticket = self.get_object()
try:
message = ticket.messages.get(id=message_id)
except TicketMessage.DoesNotExist:
return HttpResponseBadRequest()
return JsonResponse({
'message': get_template('ticket/message.html').render({'message': message}, request),
'notification': {
'title': _('New Ticket Message For: %s') % ticket.title,
'body': truncatechars(message.body, 200),
},
})