diff --git a/ticketing/admin.py b/ticketing/admin.py index 83d7caf..7dabd6e 100644 --- a/ticketing/admin.py +++ b/ticketing/admin.py @@ -15,5 +15,8 @@ class TicketAdmin(admin.ModelAdmin): admin.site.register(Comment) admin.site.register(Template) -admin.site.register(Team) + +@admin.register(Team) +class TeamAdmin(admin.ModelAdmin): + filter_horizontal = ('members',) admin.site.register(FileAttachment) diff --git a/ticketing/migrations/0013_team_access_non_category_tickets.py b/ticketing/migrations/0013_team_access_non_category_tickets.py new file mode 100644 index 0000000..00b39c9 --- /dev/null +++ b/ticketing/migrations/0013_team_access_non_category_tickets.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.3 on 2024-03-24 14:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("ticketing", "0012_seed_mailtemplates"), + ] + + operations = [ + migrations.AddField( + model_name="team", + name="access_non_category_tickets", + field=models.BooleanField(default=False), + ), + ] diff --git a/ticketing/models.py b/ticketing/models.py index 743620e..bbad158 100644 --- a/ticketing/models.py +++ b/ticketing/models.py @@ -18,6 +18,7 @@ class Team(models.Model): name = models.CharField(max_length=200) description = models.TextField(blank=True) members = models.ManyToManyField(PawUser) + access_non_category_tickets = models.BooleanField(default=False) def __str__(self): return self.name @@ -67,6 +68,43 @@ class Ticket(models.Model): models.Index(fields=["priority", "title"]), ] + @classmethod + def _get_tickets(cls, user) -> models.QuerySet: + """ + For regular users with no team: return all open tickets that are created by the user + """ + user_teams = user.team_set.all() + if not user_teams: + return cls.objects.filter(user=user) + + q = cls.objects.filter( + models.Q(user=user) | # tickets created by user + (models.Q(assigned_team__in=user_teams) | models.Q(assigned_to=user)) | # tickets assigned to user or user's team + (models.Q(assigned_team=None) & models.Q(category=None)) # tickets that are not assigned and have no category (general), needs to be excluded with filter + ) + + if not any([team.access_non_category_tickets for team in user_teams]): + return q.exclude(models.Q(assigned_team=None) & models.Q(category=None) & ~models.Q(user=user)) + return q + + @classmethod + def get_open_tickets(cls, user) -> models.QuerySet: + """ + For regular users with no team: return all open tickets that are created by the user + """ + return cls._get_tickets(user).exclude(status=cls.Status.CLOSED) + + @classmethod + def get_closed_tickets(cls, user) -> models.QuerySet: + """ + For regular users with no team: return all closed tickets that are created by the user + """ + return cls._get_tickets(user).filter(status=cls.Status.CLOSED) + + def can_open(self, user): + return self in Ticket._get_tickets(user) + + def close_ticket(self): self.status = self.Status.CLOSED self.save() diff --git a/ticketing/views.py b/ticketing/views.py index ec5b207..364c3da 100644 --- a/ticketing/views.py +++ b/ticketing/views.py @@ -8,15 +8,7 @@ from .forms import CommentForm, TicketForm, TemplateForm, TeamAssignmentForm, Ca @login_required def show_tickets(request): - if request.user.is_staff: - # show only tickets that are not closed and are not assigned or assigned to the current user's team - tickets = Ticket.objects.filter( - ~Q(status=Ticket.Status.CLOSED) & (~Q(assigned_team=None) - | ~Q(assigned_team__in=request.user.team_set.all())) - ).order_by("priority", "-created_at") - else: - tickets = Ticket.objects.filter( - user=request.user).order_by("-created_at") + tickets = Ticket.get_open_tickets(request.user).order_by("priority", "-updated_at") return render(request, "ticketing/tickets.html", {"tickets": tickets}) @@ -37,7 +29,7 @@ def show_ticket(request, ticket_id): ticket = get_object_or_404(Ticket, pk=ticket_id) # comment_templates = Template.objects.filter(category=ticket.category) - if request.user != ticket.user and not request.user.is_staff: + if not ticket.can_open(request.user): return redirect("all_tickets") form, template_form, team_assignment_form, category_assignment_form = CommentForm( -- 2.51.2 From fa7326a1e9018714979ec16763390a1033b11558 Mon Sep 17 00:00:00 2001 From: Dustin Kroeger Date: Sun, 24 Mar 2024 19:57:55 +0100 Subject: [PATCH 2/9] Allow ReadOnly-Roles --- paw/templates/ticketing/ticket_detail.html | 16 ++++++++-------- .../migrations/0014_team_readonly_access.py | 18 ++++++++++++++++++ ticketing/models.py | 9 ++++++++- ticketing/views.py | 3 ++- 4 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 ticketing/migrations/0014_team_readonly_access.py diff --git a/paw/templates/ticketing/ticket_detail.html b/paw/templates/ticketing/ticket_detail.html index 0534409..dde525b 100644 --- a/paw/templates/ticketing/ticket_detail.html +++ b/paw/templates/ticketing/ticket_detail.html @@ -36,7 +36,7 @@

{% trans 'Activity' %}

{% for comment in comments %} - {% if not comment.is_only_for_staff or comment.is_only_for_staff and request.user.is_staff %} + {% if not comment.is_only_for_staff or comment.is_only_for_staff and can_edit %} {% if comment.user == request.user %}
{% else %} @@ -62,7 +62,7 @@
{% if ticket.status != 'closed' %} - {% if request.user.is_staff %} + {% if can_edit %}
{% csrf_token %}
@@ -88,7 +88,7 @@
- {% if request.user.is_staff %} + {% if can_edit %} {% endif %}
@@ -106,14 +106,14 @@ {% else %}
{% trans 'Ticket has been closed' %}
- {% if request.user.is_staff and ticket.status == 'closed' %} + {% if can_edit and ticket.status == 'closed' %}
{% csrf_token %}
- {% endif %} {% comment %} request.user.is_staff and ticket.status == 'closed' {% endcomment %} + {% endif %} {% comment %} can_edit and ticket.status == 'closed' {% endcomment %} {% endif %}
@@ -162,7 +162,7 @@ {% trans 'General' %} {% endif %} - {% if request.user.is_staff %} + {% if can_edit %}
{% csrf_token %}

{% trans 'Assign to new category' %}

@@ -177,7 +177,7 @@
{% include 'partials/assigned_to.html' with assigned_to=ticket.assigned_to %} - {% if request.user.is_staff and ticket.assigned_to != request.user %} + {% if can_edit and ticket.assigned_to != request.user %} {% csrf_token %} @@ -192,7 +192,7 @@ {% trans 'Unassigned' %} {% endif %}
- {% if request.user.is_staff %} + {% if can_edit %} {% csrf_token %}

{% trans 'Assign to new team' %}

diff --git a/ticketing/migrations/0014_team_readonly_access.py b/ticketing/migrations/0014_team_readonly_access.py new file mode 100644 index 0000000..9766b41 --- /dev/null +++ b/ticketing/migrations/0014_team_readonly_access.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.3 on 2024-03-24 18:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("ticketing", "0013_team_access_non_category_tickets"), + ] + + operations = [ + migrations.AddField( + model_name="team", + name="readonly_access", + field=models.BooleanField(default=False), + ), + ] diff --git a/ticketing/models.py b/ticketing/models.py index bbad158..13b2dde 100644 --- a/ticketing/models.py +++ b/ticketing/models.py @@ -19,6 +19,7 @@ class Team(models.Model): description = models.TextField(blank=True) members = models.ManyToManyField(PawUser) access_non_category_tickets = models.BooleanField(default=False) + readonly_access = models.BooleanField(default=False) def __str__(self): return self.name @@ -102,7 +103,13 @@ class Ticket(models.Model): return cls._get_tickets(user).filter(status=cls.Status.CLOSED) def can_open(self, user): - return self in Ticket._get_tickets(user) + return self.user == user or self.assigned_to == user or self.assigned_team in user.team_set.all() or self.assigned_team is None and user.team_set.filter(access_non_category_tickets=True).exists() + + def can_edit(self, user): + assigned_and_write_access = self.assigned_team in user.team_set.filter(readonly_access=False) + unassigned_and_write_access = self.assigned_team is None and user.team_set.filter(access_non_category_tickets=True, readonly_access=False).exists() + print(assigned_and_write_access, unassigned_and_write_access) + return self.can_open(user) and (assigned_and_write_access or unassigned_and_write_access) def close_ticket(self): diff --git a/ticketing/views.py b/ticketing/views.py index 364c3da..4e49e51 100644 --- a/ticketing/views.py +++ b/ticketing/views.py @@ -76,7 +76,8 @@ def show_ticket(request, ticket_id): context = { "ticket": ticket, "comments": comments, "attachments": [attachment.file for attachment in ticket.fileattachment_set.all()], "form": form, "template_form": template_form, - "team_assignment_form": team_assignment_form, "category_assignment_form": category_assignment_form + "team_assignment_form": team_assignment_form, "category_assignment_form": category_assignment_form, + "can_edit": ticket.can_edit(request.user) } return render(request, "ticketing/ticket_detail.html", context) -- 2.51.2 From 31047a4fa295e3c49f9992f453ffca0c0592bc0f Mon Sep 17 00:00:00 2001 From: Dustin Kroeger Date: Sun, 24 Mar 2024 20:12:55 +0100 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=91=8C=20IMPROVE:=20Add=20Superuser?= =?UTF-8?q?=20(Admin)=20privileges=20and=20extend=20view=20changes=20for?= =?UTF-8?q?=20new=20permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paw/templates/ticketing/ticket_detail.html | 4 +++- ticketing/forms.py | 2 +- ticketing/models.py | 9 ++++++++- ticketing/views.py | 15 ++++++++------- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/paw/templates/ticketing/ticket_detail.html b/paw/templates/ticketing/ticket_detail.html index dde525b..4e729bb 100644 --- a/paw/templates/ticketing/ticket_detail.html +++ b/paw/templates/ticketing/ticket_detail.html @@ -62,6 +62,7 @@
{% if ticket.status != 'closed' %} + {% if can_edit or ticker.user == request.user %} {% if can_edit %} {% csrf_token %} @@ -94,7 +95,7 @@
- {% if request.user.is_staff %} + {% if can_edit %}
+ {% endif %} {% comment %} can_edit or ticket.user == request.user {% endcomment %} {% else %}
{% trans 'Ticket has been closed' %}
{% if can_edit and ticket.status == 'closed' %} diff --git a/ticketing/forms.py b/ticketing/forms.py index 94f39d2..ec8f04b 100644 --- a/ticketing/forms.py +++ b/ticketing/forms.py @@ -96,7 +96,7 @@ class TemplateForm(forms.Form): class TeamAssignmentForm(forms.Form): - team_select = forms.ModelChoiceField(queryset=Team.objects.all(), empty_label=_('No Team'), required=False, widget=forms.Select( + team_select = forms.ModelChoiceField(queryset=Team.objects.filter(readonly_access=False), empty_label=_('No Team'), required=False, widget=forms.Select( attrs={'class': 'select select-bordered select-sm w-full'})) diff --git a/ticketing/models.py b/ticketing/models.py index 13b2dde..62a8819 100644 --- a/ticketing/models.py +++ b/ticketing/models.py @@ -74,6 +74,9 @@ class Ticket(models.Model): """ For regular users with no team: return all open tickets that are created by the user """ + if user.is_superuser: + return cls.objects.all() + user_teams = user.team_set.all() if not user_teams: return cls.objects.filter(user=user) @@ -103,10 +106,14 @@ class Ticket(models.Model): return cls._get_tickets(user).filter(status=cls.Status.CLOSED) def can_open(self, user): + if user.is_superuser: + return True return self.user == user or self.assigned_to == user or self.assigned_team in user.team_set.all() or self.assigned_team is None and user.team_set.filter(access_non_category_tickets=True).exists() def can_edit(self, user): - assigned_and_write_access = self.assigned_team in user.team_set.filter(readonly_access=False) + if user.is_superuser: + return True + assigned_and_write_access = self.assigned_team in user.team_set.filter(readonly_access=False) or self.assigned_to == user unassigned_and_write_access = self.assigned_team is None and user.team_set.filter(access_non_category_tickets=True, readonly_access=False).exists() print(assigned_and_write_access, unassigned_and_write_access) return self.can_open(user) and (assigned_and_write_access or unassigned_and_write_access) diff --git a/ticketing/views.py b/ticketing/views.py index 4e49e51..77babb1 100644 --- a/ticketing/views.py +++ b/ticketing/views.py @@ -27,6 +27,7 @@ def show_tickets_history(request): @login_required def show_ticket(request, ticket_id): ticket = get_object_or_404(Ticket, pk=ticket_id) + can_edit = ticket.can_edit(request.user) # comment_templates = Template.objects.filter(category=ticket.category) if not ticket.can_open(request.user): @@ -36,25 +37,25 @@ def show_ticket(request, ticket_id): ), TemplateForm(), TeamAssignmentForm(), CategoryAssignmentForm() if request.method == "POST": - if 'apply_template' in request.POST and request.user.is_staff: + if 'apply_template' in request.POST and can_edit: template_form = TemplateForm(request.POST) if template_form.is_valid(): template = template_form.cleaned_data["template_select"] form = CommentForm(initial={"text": template.content}) - elif 'assign_to_team' in request.POST and request.user.is_staff: + elif 'assign_to_team' in request.POST and can_edit: team_assignment_form = TeamAssignmentForm(request.POST) if team_assignment_form.is_valid(): ticket.assign_to_team( team_assignment_form.cleaned_data["team_select"]) - elif 'assign_to_category' in request.POST and request.user.is_staff: + elif 'assign_to_category' in request.POST and can_edit: category_assignment_form = CategoryAssignmentForm(request.POST) if category_assignment_form.is_valid(): ticket.category = category_assignment_form.cleaned_data["category_select"] ticket.save() - elif 'assign_self' in request.POST and request.user.is_staff: + elif 'assign_self' in request.POST and can_edit: ticket.assigned_to = request.user ticket.save() - elif 'reopen_ticket' in request.POST and request.user.is_staff: + elif 'reopen_ticket' in request.POST and can_edit: ticket.status = Ticket.Status.IN_PROGRESS ticket.save() else: @@ -69,7 +70,7 @@ def show_ticket(request, ticket_id): for file in form.cleaned_data["attachments"]: ticket.fileattachment_set.create(file=file) - if 'close' in request.POST and request.user.is_staff: + if 'close' in request.POST and can_edit: ticket.close_ticket() comments = ticket.comment_set.all() @@ -77,7 +78,7 @@ def show_ticket(request, ticket_id): "ticket": ticket, "comments": comments, "attachments": [attachment.file for attachment in ticket.fileattachment_set.all()], "form": form, "template_form": template_form, "team_assignment_form": team_assignment_form, "category_assignment_form": category_assignment_form, - "can_edit": ticket.can_edit(request.user) + "can_edit": can_edit } return render(request, "ticketing/ticket_detail.html", context) -- 2.51.2 From 7e22b67ed2622fd2e4dedc2319825c037ed0292c Mon Sep 17 00:00:00 2001 From: Dustin Kroeger Date: Sun, 24 Mar 2024 20:18:11 +0100 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20Hide=20tailwindcss=20?= =?UTF-8?q?from=20language=20stats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c4b1fab --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +paw/static/css/paw.css -linguist-vendored \ No newline at end of file -- 2.51.2 From 74f3db6b3bf87462c590e15621d8af58810fbd06 Mon Sep 17 00:00:00 2001 From: Dustin Kroeger Date: Sun, 24 Mar 2024 20:25:17 +0100 Subject: [PATCH 5/9] update attributes --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index c4b1fab..79660a1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -paw/static/css/paw.css -linguist-vendored \ No newline at end of file +paw/static/css/paw.css linguist-vendored \ No newline at end of file -- 2.51.2 From adfad674f0b6e7152e394ea7d418352763b5a717 Mon Sep 17 00:00:00 2001 From: Dustin Kroeger Date: Sun, 24 Mar 2024 20:28:05 +0100 Subject: [PATCH 6/9] change attributes to generated --- .gitattributes | 1 - paw/static/.gitattributes | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 .gitattributes create mode 100644 paw/static/.gitattributes diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 79660a1..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -paw/static/css/paw.css linguist-vendored \ No newline at end of file diff --git a/paw/static/.gitattributes b/paw/static/.gitattributes new file mode 100644 index 0000000..f3d9925 --- /dev/null +++ b/paw/static/.gitattributes @@ -0,0 +1 @@ +css/paw.css linguist-generated \ No newline at end of file -- 2.51.2 From a55b9e5994d14642134d3a1e7a7cb39075178f4b Mon Sep 17 00:00:00 2001 From: Dustin Kroeger Date: Sun, 24 Mar 2024 21:13:32 +0100 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20fix=20typo=20for=20ti?= =?UTF-8?q?cket=20user?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- paw/templates/ticketing/ticket_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paw/templates/ticketing/ticket_detail.html b/paw/templates/ticketing/ticket_detail.html index 4e729bb..a4aa3a3 100644 --- a/paw/templates/ticketing/ticket_detail.html +++ b/paw/templates/ticketing/ticket_detail.html @@ -62,7 +62,7 @@
{% if ticket.status != 'closed' %} - {% if can_edit or ticker.user == request.user %} + {% if can_edit or ticket.user == request.user %} {% if can_edit %}
{% csrf_token %} -- 2.51.2 From 646172dc935fc64c29608bfbb294bd8994e75786 Mon Sep 17 00:00:00 2001 From: Dustin Kroeger Date: Sun, 24 Mar 2024 21:13:47 +0100 Subject: [PATCH 8/9] improve status display --- ticketing/models.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ticketing/models.py b/ticketing/models.py index 62a8819..22d4da1 100644 --- a/ticketing/models.py +++ b/ticketing/models.py @@ -135,6 +135,9 @@ class Ticket(models.Model): def get_priority(self): return self.Priority(self.priority).label + + def get_status(self): + return self.Status(self.status).label def __str__(self): return self.title @@ -177,7 +180,7 @@ def send_mail_notification(sender, instance, created, **kwargs): 'ticket_description': instance.description, 'ticket_category': instance.category.name if instance.category else _('General')}) @receiver(pre_save, sender=Ticket, dispatch_uid="mail_change_notification") -def send_mail_change_notification(sender, instance, update_fields=None, **kwargs): +def send_mail_change_notification(sender, instance: Ticket, update_fields=None, **kwargs): if not instance.user.receive_email_notifications: return None try: @@ -190,8 +193,8 @@ def send_mail_change_notification(sender, instance, update_fields=None, **kwargs if not mail_template: return None mail_template.send_mail(instance.user.email, { - 'ticket_id': instance.id, 'ticket_creator_username': instance.user.username, 'ticket_status': instance.status, - 'ticket_status_old': old_instance.status, 'ticket_title': instance.title + 'ticket_id': instance.id, 'ticket_creator_username': instance.user.username, 'ticket_status': instance.get_status(), + 'ticket_status_old': old_instance.get_status(), 'ticket_title': instance.title }) class Comment(models.Model): -- 2.51.2 From 11a1db028024d2c05f75919ef5ebaebab7be5ad0 Mon Sep 17 00:00:00 2001 From: Dustin Kroeger Date: Sun, 24 Mar 2024 21:15:32 +0100 Subject: [PATCH 9/9] Update version to 0.5.0-beta.0 --- paw/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paw/__init__.py b/paw/__init__.py index e6f2b72..d6e99ac 100644 --- a/paw/__init__.py +++ b/paw/__init__.py @@ -1,5 +1,5 @@ from django import get_version -VERSION = (0, 4, 0, "beta", 1) +VERSION = (0, 5, 0, "beta", 0) __version__ = get_version(VERSION)