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), }, } )