Compare commits

...

115 Commits

Author SHA1 Message Date
7d408193f9 Merge branch 'jones-dev' of https://gitea.corbz.dev/corbz/ticket-website into origin/jones-dev 2024-03-06 16:06:30 +00:00
2e4eff5f3a Delete useless files 2024-03-06 16:06:25 +00:00
fc992c75a6 Comment out annoying file logger 2024-03-06 16:05:49 +00:00
fc8cea1d0a Display department and priority 2024-03-06 16:05:29 +00:00
92c6cda583 Update Dockerfile 2024-03-06 16:02:27 +00:00
75531f872b corrected static paths 2024-03-06 16:02:15 +00:00
c9e47e3e9e Dashboard view to template 2024-03-06 16:01:46 +00:00
4890d7a4c3 Merge branch 'jones-dev' of https://gitea.corbz.dev/corbz/ticket-website into jones-dev 2024-01-26 17:37:43 +00:00
c35428792e example images 2024-01-26 17:37:42 +00:00
c94775d93c Update apps/home/models.py 2024-01-24 13:24:32 +00:00
2691380f81 Update & create scripts for linux & windows 2024-01-23 12:11:43 +00:00
950676a9fe Cleaned up fixture filenames & removed old fixtures 2024-01-23 12:11:18 +00:00
7c00552714 Commenting & segmenting into functions 2024-01-23 12:10:42 +00:00
6effc6799e tooltip on ticket item department & priority 2024-01-23 11:34:38 +00:00
3446c3b760 Upgrade to javascript bootstrap 5.3 2024-01-23 11:33:14 +00:00
1c5282e6c9 Removed the ticket item complex details change 2024-01-23 10:32:39 +00:00
dfef67642f Fix js error when author had no deparment 2024-01-23 09:54:50 +00:00
6f6a0f8632 Persistent state for complex ticket info 2024-01-23 09:41:33 +00:00
e342680351 User Theme saved to local storage
The theme will no longer reset to the default on page change or reload.
2024-01-23 09:18:31 +00:00
4d7f2ba3ea working on department/priority item badge 2024-01-23 08:04:27 +00:00
bf6a004d64 Merge branch 'jones-dev' of https://gitea.corbz.dev/corbz/ticket-website into origin/jones-dev 2024-01-22 16:34:43 +00:00
a482be8883 Strict tags option & css changes 2024-01-22 16:32:48 +00:00
2364277c1a Department colours and order 2024-01-22 16:31:59 +00:00
2fb6ea6c64 Updated none-found and ticket item priority badge 2024-01-22 14:14:44 +00:00
f6568230b1 Filter spam prevention #7 2024-01-22 14:14:08 +00:00
97141dec4d TicketTag/Priority order field 2024-01-22 14:13:23 +00:00
62bd7de0be Scrollbar updater 2024-01-22 11:51:10 +00:00
c32eb91d03 Reset Ticket Items Scrollbar on State Change #17
I've added a line in the updateItemsState function to reset the scrollbar to the top when the state changes.

This fixes #17
2024-01-22 11:33:15 +00:00
3f04f3561d Merge pull request 'origin/jones-dev' (#18) from origin/jones-dev into jones-dev
Reviewed-on: https://gitea.corbz.dev/corbz/ticket-website/pulls/18
2024-01-22 11:22:49 +00:00
863f5cf6ec Quick & Hacky Theme Toggler
Just to add the base functionality, will improve upon this later.
2024-01-22 11:15:57 +00:00
4761db6541 Updated & Isolated PerfectScrollbar JS/CSS 2024-01-22 11:14:53 +00:00
368cdc6359 default to light mode for dev
because apps should always be made light mode first.

Makes it easier to implement nicer dark mode later on.
2024-01-22 01:10:25 +00:00
90abffac05 Ticket Item Changes & Ticket.string_datetime
Added string datetime and indicators for priority and departments.

Also added a toggle for the indicators/departments to be shown.
2024-01-22 01:05:31 +00:00
4344dfb271 Remove commented code & remake tags dropdown 2024-01-21 23:12:01 +00:00
3f7127347b Increased Ticket.short_description length to 200
Previously 150.

I also added a check to see if 200 is exceeded before suffixing the "..." to it.
2024-01-21 22:53:00 +00:00
4647236e20 Clear filter button & item priority indicator 2024-01-21 22:52:11 +00:00
25fbfb3aa1 Made default ticket colours more vibrant 2024-01-20 20:47:39 +00:00
ead8ae4171 theme depended subtle border colours 2024-01-20 20:47:23 +00:00
5957ddf8f1 Fixed Issue Making Django Suggest makemigrations
Was using BASE_DIR in the default image path for the authentication.User model.

This is a problem because BASE_DIR may change with the host machine, which it did in my case.

Django sees the changed path and incorrectly recommends making migrations again.
2024-01-20 19:50:20 +00:00
c9e59c5c66 tags and priorities now shown in ticket items 2024-01-19 17:56:10 +00:00
4e70005cac Visual fixes and changes for tickets page 2024-01-19 17:55:10 +00:00
c62b975670 Fix conflicts with bootstrap css 2024-01-19 17:54:34 +00:00
bd57cfb94d Added short_description property to Ticket
returns a cleaned version of the description limited to 150 characters.

Suffixed with "...".
2024-01-19 17:54:11 +00:00
880bab8d33 Implement dynamic field blacklist/whitelist 2024-01-19 17:52:25 +00:00
06e00f3737 Changed some colours for tickets (light/dark mode) 2024-01-19 10:05:15 +00:00
3d9da2d729 Gave currently active ticket ".active" class.
Implemented tracking for this, over the previous code which tracked the active ticket by checking it's ".bgc-grey-100" class.

This change was made because (its better and) the previous implementation was incompatible with dark mode support.
2024-01-19 10:04:17 +00:00
187153bb7c Fixed and restyled PerfectScrollbars 2024-01-19 10:02:43 +00:00
bb28e2b2dc Redid migrations to fix issue where 004 isnt detected 2024-01-19 10:00:40 +00:00
01da177cd1 Dark mode implementation step 1 2024-01-18 23:44:47 +00:00
f97bccbcec Ticket Content Loading View 2024-01-18 23:44:35 +00:00
d9ddc0656d Create 0004_alter_user_icon.py 2024-01-18 20:22:19 +00:00
9f65fcab71 fixed invalid page error
The page would be above 1, and then would query the API with a new filter that reduced the results to a single page, causing an error.
2024-01-18 20:22:15 +00:00
9cff951b4b description for API View 2024-01-18 20:01:51 +00:00
506ed5e338 Ticket JavaScript rewrite 2024-01-18 20:00:46 +00:00
24a65014b9 fix broken proflie icons 2024-01-18 20:00:11 +00:00
c8b0b9982d Delete .gitkeep 2024-01-18 12:17:17 +00:00
283f0f3099 Created Tons of Dummy Tickets
This can be used to test pagination, and feel for how the application experience will be under production.
2024-01-18 12:17:11 +00:00
d48da63909 Improved filter count API 2024-01-18 12:16:32 +00:00
e5ffbc9005 Restructure of settings.py 2024-01-18 12:16:07 +00:00
067bc744d7 Delete fixtures.bat 2024-01-18 12:15:47 +00:00
8d4050ce8f Create runserver.sh
Nice script to run the server.
Migrates before hand.
Runs on 0.0.0.0:8000 with the --noreload argument.
2024-01-18 12:15:42 +00:00
6df16f054b Staticfiles Restructure - removal of assets/
Removed meaningless assets/ folder within static.

I believe its an artifact from an older Django version, but it needed to be removed to reduce clutter and use {% static %} tags.
2024-01-18 12:15:08 +00:00
a3b62f4a8e Fixed scrollbar issues 2024-01-18 00:13:09 +00:00
da1372de6f Rate limit alert 2024-01-17 23:39:47 +00:00
917b904790 Remove unused code 2024-01-17 23:39:38 +00:00
eb2c8439fe Implement API rate limits • 1000/day 2024-01-17 23:19:16 +00:00
79145b1e84 fix ticket template issues 2024-01-17 23:18:53 +00:00
9dce656a48 Migrate to bootstrap 5.3 2024-01-17 23:18:33 +00:00
b437071e6a Split css into several components 2024-01-17 22:12:15 +00:00
aa1ef52694 integrate API changes into frontend 2024-01-17 21:54:54 +00:00
77dc45044b moved fixtures into app folders 2024-01-17 21:54:35 +00:00
70618f2bdb Working on template for listed tickets 2024-01-17 19:33:49 +00:00
307ed35ee9 API Tokens and Filters 2024-01-17 19:33:28 +00:00
88942a84ba Fix scrolling bug on ticket page
On smaller screens, expanding the filter sidebar would create scrollable whitespace below the page.

Hiding the .full-container overflow seems to patch the issue, although investigation is required to find a smoother patch.
2024-01-17 11:16:05 +00:00
9509bffe66 Using searchbar trigger loading view 2024-01-16 23:54:44 +00:00
2586dcc944 Author name added to search filter 2024-01-16 23:54:15 +00:00
c7a185a919 Sidebar layout change + loading placeholder 2024-01-16 23:54:01 +00:00
2d0b8762b0 timed log file 2024-01-16 23:53:31 +00:00
4c37286b59 Improved logging
NOTE:
when running without --noreload, there will be two log files, as Django runs two threads to make the auto reloading functionality work.
2024-01-16 16:53:27 +00:00
b52d1376d2 Updated default priority colour 2024-01-16 16:30:53 +00:00
1a79fe3916 Added fixture for tickets with unique authors 2024-01-16 16:25:28 +00:00
e5930668c5 Tag Strict/Loose filter controls (non-functional)
Controls to specify whether to use the AND or OR operators when applying ticket filters.

I.e:
- Only show tickets with tag1 AND tag2
- Show tickets matching either tag1 OR tag2
2024-01-16 16:22:57 +00:00
dc21810add Open tickets on mobile close filter sidebar
Added line to close the filter sidebar when selecting a ticket to open on a mobile screen.
2024-01-16 16:21:14 +00:00
b8751ba315 Added custom border-radius and bg css
Added .rounded-top-0, end-0, bottom-0, start-0, for setting the respective side to have border-radius of 0.

Also added .border-none for setting boder to none.

Added bg-none for setting background to none.
2024-01-16 16:20:29 +00:00
970d6c0307 ticket API changed to detect "__in" key 2024-01-16 13:53:05 +00:00
db8206b1bd Changed departments to radio buttons 2024-01-16 13:52:41 +00:00
eb2ae9ce6b Ticket Navigation Buttons
Added buttons to move to the next or prev ticket from the current selected ticket.

Also segmented JS into functions.
2024-01-16 13:34:13 +00:00
3ef8cd20d0 Loading animation for filter and searching tickets 2024-01-16 11:23:10 +00:00
3cfdee610c "not found" message when tickets api returns none 2024-01-16 11:08:14 +00:00
546ec2ca84 Fixed #6 - Double Tickets with Filters.
Instead of applying a list of __in filters at once, iterate over the list and apply them individually.

Only then will the tickets be filtered to match them all as intended.
2024-01-16 10:04:13 +00:00
e8df39ee9e Implemented Searchbox filter 2024-01-15 23:31:50 +00:00
73eeedbb36 tickets API changes & search box function 2024-01-15 00:27:39 +00:00
df82417790 Moving to use API in home app 2024-01-14 01:51:56 +00:00
d8c608a03c Admin Models (authentication) 2024-01-14 01:51:33 +00:00
0670382172 Admin Models 2024-01-14 01:51:00 +00:00
a708a8b151 Working on Ticket API
Improved filters and added TicketCount API View.
2024-01-14 01:50:22 +00:00
a20fba656a Implement Logging and Pathlib 2024-01-14 01:49:35 +00:00
7294638493 Removed unused template pages. 2024-01-14 01:49:16 +00:00
a876bbe7fc Added vscode launch file to help with debugging 2024-01-14 01:49:00 +00:00
ee585ed4b9 Update requirements.txt
django rest framework
2024-01-14 01:48:30 +00:00
362c8f0a16 implemented rest into home app 2024-01-12 19:25:45 +00:00
e0e16efb71 rest api implementation 2024-01-12 19:25:25 +00:00
690bec9b24 functional filter + add modal 2024-01-10 23:15:43 +00:00
4c7a81e076 default priority 2024-01-10 18:06:24 +00:00
33c721c5a6 added footer colour
some tickets were visible below the footer because it was transparent, so I gave it a background colour.
2024-01-10 11:08:27 +00:00
2dbc08b93c fixed issues with multiple filters 2024-01-10 11:07:50 +00:00
f581e22a78 fixed email content not showing on small screens #2 2024-01-10 09:34:38 +00:00
03b34ebd20 working on filter functionality #4 2024-01-10 00:48:41 +00:00
8acb8c3aca update fixture scripts 2024-01-09 22:48:41 +00:00
cae5ac024e implemented filter counts #4
While this contributes to #4, it doesn't complete it as the filters are still mostly eye candy.
2024-01-09 22:48:28 +00:00
7ccdbc326a fixtures import script (windows) 2024-01-09 00:22:14 +00:00
b0c72bdc0a added shellscript to quickly install fixtures 2024-01-09 00:20:46 +00:00
8c4a06bb33 added default fixture for tickets 2024-01-09 00:17:55 +00:00
c229413f49 Update README.md 2024-01-08 23:42:28 +00:00
03de8540b0 moved from migrations to fixtures for default data 2024-01-08 23:42:21 +00:00
106 changed files with 85101 additions and 78533 deletions

8
.gitignore vendored
View File

@ -28,10 +28,10 @@ venv
package-lock.json
staticfiles/*
!staticfiles/.gitkeep
!staticfiles/.gitkeep
.vscode/symbols.json
apps/static/assets/node_modules
apps/static/assets/yarn.lock
apps/static/assets/.temp
apps/static/node_modules
apps/static/yarn.lock
apps/static/.temp

18
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Django",
"type": "python",
"request": "launch",
"python": "${workspaceFolder}/venv/bin/python",
"program": "${workspaceFolder}/manage.py",
"args": ["runserver"],
"django": true,
"justMyCode": true
}
]
}

View File

@ -1,18 +1,27 @@
FROM python:3.9
FROM python:3.12
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV DJANGO_SETTINGS_MODULE core.settings
WORKDIR /website
COPY requirements.txt .
# install python dependencies
COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
COPY . /website/
# running migrations
# collect static files
RUN python manage.py collectstatic --noinput
# run migrations
RUN python manage.py migrate
# Port that the site runs on
EXPOSE 4411
# gunicorn
CMD ["gunicorn", "--config", "gunicorn-cfg.py", "core.wsgi"]
CMD ["gunicorn", "core.wsgi:application", "--bind", "0.0.0.0:4411"]

View File

@ -1,20 +1,65 @@
# Contributing
This guide explains how to contribute.
## Requirements
Django and other related libraries aren't included with python by default, and need to be installed:
1. Setup a python virtual environment
`python -m venv venv`
2. Activate the virtual environment
`source venv/bin/activate` (linux)
`./venv/Scripts/activate.ps1` (windows)
3. Install the pip dependencies
`pip install -r requirements.txt`
## Migrations
If facing migration errors, delete the contents of all `migrations` folders that are numerical (e.g. `0001_initial.py`) and delete the `db.sqlite3` database file. Note you will lose all data.
Migration files instruct the database on how to create tables and other entities.
To get started:
Migration files have already been created, you can import them with:
1. `python manage.py makemigrations authentication`
2. `python manage.py makemigrations home`
3. `python manage.py migrate`
`python manage.py migrate`
## Run the server
If you make changes to the models, you will need to update the migrations:
`python manage.py runserver 0.0.0.0:8000`
Available app names are:
## Access the website
- "home"
- "authentication"
`http://localhost:8000`
`python manage.py makemigrations <APP_NAME>`
## Fixtures
Fixtures allow you to import default data into the database.
They is very useful for testing purposes.
The following fixture options are available:
- `authentication/defaultuser.json` add some default users (<admin@mail.com> & <user@mail.com>, password=password)
- `authentication/department.json` add some default departments
- `home/ticketpriority.json` add some default ticket priorities
- `home/tickettag.json` add some default ticket tags
- `home/defaulttickets.json` add some default tickets (depends on ticketpriority and tickettag fixtures)
You can import a fixture using the following command (remember to replace `<OPTION>` with one of the above options):
`python manage.py loaddata apps/fixtures/<OPTION>`
## Running
1. Run the development server
`python manage.py runserver`
2. Access the website
`https://localhost:8000`

3
apps/api/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

8
apps/api/config.py Normal file
View File

@ -0,0 +1,8 @@
# -*- encoding: utf-8 -*-
from django.apps import AppConfig
class MyConfig(AppConfig):
name = 'apps.api'
label = 'apps_api'

3
apps/api/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

156
apps/api/serializers.py Normal file
View File

@ -0,0 +1,156 @@
# -*- encoding: utf-8 -*-
import logging
from django.apps import apps
from django.conf import settings
from rest_framework import serializers
from apps.home.models import Ticket, TicketPriority, TicketTag
from apps.authentication.models import Department
log = logging.getLogger(__name__)
class DynamicModelSerializer(serializers.ModelSerializer):
"""For use with GET requests, to specify which fields to include or exclude
Mimics some graphql functionality.
Usage: Inherit your ModelSerializer with this class. Add "only_fields" or
"exclude_fields" to the query parameters of your GET request.
This also works with nested foreign keys, for example:
?only_fields=name,age&company__only_fields=id,name
Some more examples:
?only_fields=company,name&company__exclude_fields=name
?exclude_fields=name&company__only_fields=id
?company__exclude_fields=name
Note: the Foreign Key serializer must also inherit from this class
"""
def only_keep_fields(self, fields_to_keep):
fields_to_keep = set(fields_to_keep.split(","))
all_fields = set(self.fields.keys())
for field in all_fields - fields_to_keep:
self.fields.pop(field, None)
def exclude_fields(self, fields_to_exclude):
fields_to_exclude = fields_to_exclude.split(",")
for field in fields_to_exclude:
self.fields.pop(field, None)
def remove_unwanted_fields(self, dynamic_params):
if fields_to_keep := dynamic_params.pop("only_fields", None):
self.only_keep_fields(fields_to_keep)
if fields_to_exclude := dynamic_params.pop("exclude_fields", None):
self.exclude_fields(fields_to_exclude)
def get_or_create_dynamic_params(self, child):
if "dynamic_params" not in self.fields[child]._context:
self.fields[child]._context.update({"dynamic_params": {}})
return self.fields[child]._context["dynamic_params"]
@staticmethod
def split_param(dynamic_param):
crumbs = dynamic_param.split("__")
return crumbs[0], "__".join(crumbs[1:]) if len(crumbs) > 1 else None
def set_dynamic_params_for_children(self, dynamic_params):
for param, fields in dynamic_params.items():
child, child_dynamic_param = self.split_param(param)
if child in set(self.fields.keys()):
dynamic_params = self.get_or_create_dynamic_params(child)
dynamic_params.update({child_dynamic_param: fields})
@staticmethod
def is_param_dynamic(p):
return p.endswith("only_fields") or p.endswith("exclude_fields")
def get_dynamic_params_for_root(self, request):
query_params = request.query_params.items()
return {k: v for k, v in query_params if self.is_param_dynamic(k)}
def get_dynamic_params(self):
"""
When dynamic params get passed down in set_context_for_children
If the child is a subclass of ListSerializer (has many=True)
The context must be fetched from ListSerializer Class
"""
if isinstance(self.parent, serializers.ListSerializer):
return self.parent._context.get("dynamic_params", {})
return self._context.get("dynamic_params", {})
def __init__(self, *args, **kwargs):
request = kwargs.get("context", {}).get("request")
super().__init__(*args, **kwargs)
is_root = bool(request)
if is_root:
if request.method != "GET":
return
dynamic_params = self.get_dynamic_params_for_root(request)
self._context.update({"dynamic_params": dynamic_params})
def to_representation(self, *args, **kwargs):
if dynamic_params := self.get_dynamic_params().copy():
self.remove_unwanted_fields(dynamic_params)
self.set_dynamic_params_for_children(dynamic_params)
return super().to_representation(*args, **kwargs)
class Meta:
abstract = True
class DepartmentSerializer(DynamicModelSerializer):
class Meta:
model = Department
fields = [
"uuid", "title", "colour", "backgroundcolour", "order"
]
class UserSerializer(DynamicModelSerializer):
department = DepartmentSerializer()
class Meta:
model = apps.get_model(settings.AUTH_USER_MODEL)
fields = [
"uuid", "icon", "email", "forename", "surname", "department",
"create_timestamp", "edit_timestamp"
]
class TicketPrioritySerializer(DynamicModelSerializer):
class Meta:
model = TicketPriority
fields = [
"uuid", "title", "colour", "backgroundcolour", "order"
]
class TicketTagSerializer(DynamicModelSerializer):
class Meta:
model = TicketTag
fields = [
"uuid", "title", "colour", "backgroundcolour", "order"
]
class TicketSerializer(DynamicModelSerializer):
author = UserSerializer()
priority = TicketPrioritySerializer()
tags = TicketTagSerializer(many=True)
class Meta:
model = Ticket
fields = (
"uuid", "title", "description", "author", "create_timestamp",
"edit_timestamp", "is_edited", "timestamp", "priority", "tags",
"short_description", "display_datetime"
)

3
apps/api/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

15
apps/api/urls.py Normal file
View File

@ -0,0 +1,15 @@
# -*- encoding: utf-8 -*-
from django.urls import path, include
from rest_framework.authtoken.views import obtain_auth_token
from . import views
urlpatterns = [
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
path("api-token-auth/", obtain_auth_token),
path("tickets/", views.TicketListApiView.as_view(), name="tickets"),
path("filter-counts/", views.FilterCountListApiView.as_view(), name="filter-counts"),
]

94
apps/api/views.py Normal file
View File

@ -0,0 +1,94 @@
# -*- encoding: utf-8 -*-
import logging
from django_filters import rest_framework as rest_filters
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions, filters, generics
from rest_framework.pagination import PageNumberPagination
from rest_framework.authentication import SessionAuthentication, TokenAuthentication
from apps.home.models import Ticket, TicketPriority, TicketTag
from apps.authentication.models import Department
from .serializers import (
TicketSerializer, TicketTagSerializer, TicketPrioritySerializer,
UserSerializer
)
log = logging.getLogger(__name__)
class TicketPaginiation(PageNumberPagination):
page_size = 25
page_size_query_param = "page_size"
max_page_size = 100
class TicketListApiView(generics.ListAPIView):
"""Returns a list of all **Tickets** on the system.
Use a querystring to filter down, order, and search the results.
Querystring options are:
- uuid
- priority
- tags
- author
- author__department
"""
authentication_classes = [SessionAuthentication, TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
pagination_class = TicketPaginiation
serializer_class = TicketSerializer
queryset = Ticket.objects.all()
filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ["uuid", "priority", "tags", "author", "author__department"]
search_fields = ["author__forename", "author__surname", "title", "description"]
ordering_fields = ["create_timestamp", "edit_timestamp"]
def get_queryset(self):
strict_tags = self.request.query_params.get("strict-tags")
if not strict_tags:
return self.queryset
tag_uuids = self.request.query_params.getlist("tags", [])
queryset = self.queryset
log.debug("tag uuids %s", tag_uuids)
for uuid in tag_uuids:
if not uuid: continue
log.debug("uuid %s", uuid)
queryset = queryset.filter(tags__uuid__in=[uuid])
return queryset
class FilterCountListApiView(generics.ListAPIView):
authentication_classes = [SessionAuthentication, TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
self._tickets = Ticket.objects.all()
data = {"tickets": self._tickets.count()}
self._fill_data(TicketPriority, data, "priority")
self._fill_data(TicketTag, data, "tags")
self._fill_data(Department, data, "department", model_key="author__department")
return Response(data, status.HTTP_200_OK)
def _fill_data(self, model, data: dict, key: str, *, model_key:str=None):
data[key] = {} # prevent KeyError
objects = model.objects.all()
for obj in objects:
data[key][str(obj.uuid)] = self._tickets.filter(**{model_key or key: obj}).count()

View File

@ -4,5 +4,26 @@ from django.contrib import admin
from .models import User, Department
admin.site.register(User)
admin.site.register(Department)
class UserAdmin(admin.ModelAdmin):
list_display = ["uuid", "forename", "surname", "email", "get_department_title", "icon", "is_staff", "is_superuser", "is_active"]
list_filter = ["department__title", "is_staff", "is_superuser", "is_active"]
ordering = ["department__title"]
@admin.display(description="Department", ordering="department__title")
def get_department_title(self, obj):
return obj.department.title if obj.department else None
class DepartmentAdmin(admin.ModelAdmin):
list_display = ["uuid", "title", "colour", "backgroundcolour", "order", "get_user_count"]
@admin.display(description="Users")
def get_user_count(self, obj):
return User.objects.filter(department=obj).count()
admin.site.register(User, UserAdmin)
admin.site.register(Department, DepartmentAdmin)

View File

@ -0,0 +1,52 @@
[
{
"model": "authentication.department",
"pk": "4e245769-6b67-4a6e-b804-54a3ceb3b8c0",
"fields": {
"title": "Development",
"colour": "#1976d2",
"backgroundcolour": "#bbdefb",
"order": 4
}
},
{
"model": "authentication.department",
"pk": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"fields": {
"title": "Marketing",
"colour": "#7b1fa2",
"backgroundcolour": "#e1bee7",
"order": 1
}
},
{
"model": "authentication.department",
"pk": "a6517555-0bcc-4baa-8e2f-798916562b1c",
"fields": {
"title": "Management",
"colour": "#689f38",
"backgroundcolour": "#dcedc8",
"order": 0
}
},
{
"model": "authentication.department",
"pk": "bae35c7a-a929-4465-b70f-03254b0774e0",
"fields": {
"title": "Sales",
"colour": "#e64a19",
"backgroundcolour": "#ffccbc",
"order": 2
}
},
{
"model": "authentication.department",
"pk": "c2bbabd7-05ac-4bc8-97a3-15bafdb478d9",
"fields": {
"title": "Business Strategy",
"colour": "#c2185b",
"backgroundcolour": "#f8bbd0",
"order": 3
}
}
]

View File

@ -0,0 +1,699 @@
[
{
"model": "authentication.user",
"pk": "06a8d977-d67d-432c-96d4-6e41993f6423",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/06a8d977-d67d-432c-96d4-6e41993f6423/icon.webp",
"email": "andy.horton@example.com",
"forename": "Andy",
"surname": "Horton",
"department": "bae35c7a-a929-4465-b70f-03254b0774e0",
"create_timestamp": "2024-01-13T21:01:27Z",
"edit_timestamp": "2024-01-13T21:02:12.965Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "07b620ee-6b47-4bca-842a-0fd6d5204eb8",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/07b620ee-6b47-4bca-842a-0fd6d5204eb8/icon.webp",
"email": "luke.wright@example.com",
"forename": "Luke",
"surname": "Wright",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T20:54:15Z",
"edit_timestamp": "2024-01-13T20:55:15.013Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "0b2cd1c5-cb96-4b23-a1cd-0a306b5894a1",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/0b2cd1c5-cb96-4b23-a1cd-0a306b5894a1/icon.webp",
"email": "carl.brooks@example.com",
"forename": "Carl",
"surname": "Brooks",
"department": "bae35c7a-a929-4465-b70f-03254b0774e0",
"create_timestamp": "2024-01-13T21:00:16Z",
"edit_timestamp": "2024-01-13T21:00:52.168Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "0b437298-7c00-47fa-a54f-d3cfdf39a0c3",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/0b437298-7c00-47fa-a54f-d3cfdf39a0c3/icon.webp",
"email": "vera.jones@example.com",
"forename": "Vera",
"surname": "Jones",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T20:39:37Z",
"edit_timestamp": "2024-01-13T20:40:30.134Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "1255e2cd-1876-452f-ab1a-bd893dcfbfff",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/1255e2cd-1876-452f-ab1a-bd893dcfbfff/icon.webp",
"email": "chris.rodriquez@example.com",
"forename": "Chris",
"surname": "Rodriquez",
"department": "c2bbabd7-05ac-4bc8-97a3-15bafdb478d9",
"create_timestamp": "2024-01-13T20:34:57Z",
"edit_timestamp": "2024-01-13T20:35:34.086Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "21b457a1-b64a-4499-8c53-0e2f3b42fe3c",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/21b457a1-b64a-4499-8c53-0e2f3b42fe3c/icon.webp",
"email": "gilbert.baker@example.com",
"forename": "Gilbert",
"surname": "Baker",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T19:59:56Z",
"edit_timestamp": "2024-01-13T20:00:58.570Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "248bc1ef-df52-445e-847c-e370dccf436a",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/248bc1ef-df52-445e-847c-e370dccf436a/icon.webp",
"email": "noah.thompson@example.com",
"forename": "Noah",
"surname": "Thompson",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T20:38:40Z",
"edit_timestamp": "2024-01-13T20:39:32.879Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "2955ba6a-bffb-48a7-9e61-b2b87ac27f3d",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/2955ba6a-bffb-48a7-9e61-b2b87ac27f3d/icon.webp",
"email": "amelia.kelley@example.com",
"forename": "Amelia",
"surname": "Kelley",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T20:56:45Z",
"edit_timestamp": "2024-01-13T20:57:23.453Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "420f5146-ed03-495a-982a-6dabefd7fd62",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/420f5146-ed03-495a-982a-6dabefd7fd62/icon.webp",
"email": "gavin.murphy@example.com",
"forename": "Gavin",
"surname": "Murphy",
"department": "4e245769-6b67-4a6e-b804-54a3ceb3b8c0",
"create_timestamp": "2024-01-13T20:30:56Z",
"edit_timestamp": "2024-01-13T20:31:45.569Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "48ec50cd-d89b-41d7-bcb0-92612f0cf104",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/48ec50cd-d89b-41d7-bcb0-92612f0cf104/icon.webp",
"email": "alexander.butler@example.com",
"forename": "Alexander",
"surname": "Butler",
"department": "4e245769-6b67-4a6e-b804-54a3ceb3b8c0",
"create_timestamp": "2024-01-13T20:37:16Z",
"edit_timestamp": "2024-01-13T20:38:00.511Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "4965745e-b82a-4496-80e1-055217a780b0",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": "2024-01-13T20:53:38.159Z",
"email": "admin@mail.com",
"forename": "Default",
"surname": "Admin User",
"department": null,
"create_timestamp": "2024-01-08T23:14:13.402Z",
"edit_timestamp": "2024-01-08T23:14:13.444Z",
"is_active": true,
"is_staff": true,
"is_superuser": true,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "4b203599-8de8-45de-b9aa-8ad88230ac2f",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/4b203599-8de8-45de-b9aa-8ad88230ac2f/icon.webp",
"email": "grace.chapman@example.com",
"forename": "Grace",
"surname": "Chapman",
"department": "c2bbabd7-05ac-4bc8-97a3-15bafdb478d9",
"create_timestamp": "2024-01-13T20:40:30Z",
"edit_timestamp": "2024-01-13T21:04:56.382Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "639f6399-60a9-4597-b9d4-d1df6680bb4a",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/639f6399-60a9-4597-b9d4-d1df6680bb4a/icon.webp",
"email": "tomothy.mitchelle@example.com",
"forename": "Tomothy",
"surname": "Mitchelle",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T19:59:17Z",
"edit_timestamp": "2024-01-13T19:59:56.374Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "64bbc7c0-6c08-4307-9740-419b0b279d2e",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/64bbc7c0-6c08-4307-9740-419b0b279d2e/icon.webp",
"email": "jerome.larson@example.com",
"forename": "Jerome",
"surname": "Larson",
"department": "4e245769-6b67-4a6e-b804-54a3ceb3b8c0",
"create_timestamp": "2024-01-13T20:33:13Z",
"edit_timestamp": "2024-01-13T20:33:54.560Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "6e0af77b-36f7-41a5-93ab-dfd6663b80d5",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/6e0af77b-36f7-41a5-93ab-dfd6663b80d5/icon.webp",
"email": "philip.martinez@example.com",
"forename": "Philip",
"surname": "Martinez",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T20:36:46Z",
"edit_timestamp": "2024-01-13T20:37:16.023Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "77b65a5b-2399-47cd-a34c-62454309a51a",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/77b65a5b-2399-47cd-a34c-62454309a51a/icon.webp",
"email": "clyde.pena@example.com",
"forename": "Clyde",
"surname": "Pena",
"department": "bae35c7a-a929-4465-b70f-03254b0774e0",
"create_timestamp": "2024-01-13T20:41:24Z",
"edit_timestamp": "2024-01-13T20:42:05.979Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "79ab62dc-878e-4563-8b82-1537de6f4a3c",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/79ab62dc-878e-4563-8b82-1537de6f4a3c/icon.webp",
"email": "brianna.rice@example.com",
"forename": "Brianna",
"surname": "Rice",
"department": "a6517555-0bcc-4baa-8e2f-798916562b1c",
"create_timestamp": "2024-01-13T20:36:07Z",
"edit_timestamp": "2024-01-13T20:36:46.505Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "811ad22b-5153-421f-bb84-6addcb8de570",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/811ad22b-5153-421f-bb84-6addcb8de570/icon.webp",
"email": "cameron.lowe@example.com",
"forename": "Cameron",
"surname": "Lowe",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T20:57:23Z",
"edit_timestamp": "2024-01-13T20:58:45.754Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "855d09bb-5919-4dff-8652-50aa1fb81b54",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/855d09bb-5919-4dff-8652-50aa1fb81b54/icon.webp",
"email": "jerry.porter@example.com",
"forename": "Jerry",
"surname": "Porter",
"department": "a6517555-0bcc-4baa-8e2f-798916562b1c",
"create_timestamp": "2024-01-13T20:29:04Z",
"edit_timestamp": "2024-01-13T20:29:49.442Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "8b504054-ab46-4866-8a7e-46aaab29e54b",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": "2024-01-13T19:58:12Z",
"email": "ricky.simmmons@example.com",
"forename": "Ricky",
"surname": "Simmmons",
"department": "c2bbabd7-05ac-4bc8-97a3-15bafdb478d9",
"create_timestamp": "2024-01-13T19:57:37Z",
"edit_timestamp": "2024-01-13T19:58:59.815Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "94ca520d-4f5a-49c6-ab2b-9fbb362c51d9",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/94ca520d-4f5a-49c6-ab2b-9fbb362c51d9/icon.webp",
"email": "arthur.flores@example.com",
"forename": "Arthur",
"surname": "Flores",
"department": "a6517555-0bcc-4baa-8e2f-798916562b1c",
"create_timestamp": "2024-01-13T20:38:00Z",
"edit_timestamp": "2024-01-13T20:38:40.002Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "9cc67b83-2ccc-454e-be4a-9c772cb23d87",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/9cc67b83-2ccc-454e-be4a-9c772cb23d87/icon.webp",
"email": "ramon.sullivan@example.com",
"forename": "Ramon",
"surname": "Sullivan",
"department": "bae35c7a-a929-4465-b70f-03254b0774e0",
"create_timestamp": "2024-01-13T20:33:54Z",
"edit_timestamp": "2024-01-13T20:34:57.626Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "9d99a266-7409-4a17-aad9-2696a4d89a7c",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/9d99a266-7409-4a17-aad9-2696a4d89a7c/icon.webp",
"email": "micheal.hart@example.com",
"forename": "Micheal",
"surname": "Hart",
"department": "4e245769-6b67-4a6e-b804-54a3ceb3b8c0",
"create_timestamp": "2024-01-13T20:51:53Z",
"edit_timestamp": "2024-01-13T20:52:21.016Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "9f469a37-4d8d-4bd0-ba4e-16b07549f42a",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/9f469a37-4d8d-4bd0-ba4e-16b07549f42a/icon.webp",
"email": "christopher.steward@example.com",
"forename": "Christopher",
"surname": "Steward",
"department": "bae35c7a-a929-4465-b70f-03254b0774e0",
"create_timestamp": "2024-01-13T20:58:45Z",
"edit_timestamp": "2024-01-13T20:59:24.489Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "a0d87275-5400-4476-8301-2d1f51808269",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": "2024-01-13T19:55:36Z",
"icon": "users/a0d87275-5400-4476-8301-2d1f51808269/icon.webp",
"email": "leo.hall@example.com",
"forename": "Leo",
"surname": "Hall",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T19:55:29Z",
"edit_timestamp": "2024-01-13T19:59:17.528Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "ac0ae9dd-e447-428e-9ef1-a9e32a638419",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/ac0ae9dd-e447-428e-9ef1-a9e32a638419/icon.webp",
"email": "claudia.thompson@example.com",
"forename": "Claudia",
"surname": "Thompson",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T20:32:34Z",
"edit_timestamp": "2024-01-13T20:33:13.643Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "b6c7372c-2006-4257-9463-20f63c840403",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/b6c7372c-2006-4257-9463-20f63c840403/icon.webp",
"email": "christopher.sanchez@example.com",
"forename": "Christopher",
"surname": "Sanchez",
"department": "c2bbabd7-05ac-4bc8-97a3-15bafdb478d9",
"create_timestamp": "2024-01-13T20:29:49Z",
"edit_timestamp": "2024-01-13T20:30:56.492Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "be2acfc8-7623-4f5f-864b-4163e07b881d",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"email": "dora.hudson@example.com",
"forename": "Dora",
"surname": "Hudson",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T20:35:34Z",
"edit_timestamp": "2024-01-13T20:36:07.313Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "c1984735-fdca-488b-b4e9-16bd01f3aebb",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/c1984735-fdca-488b-b4e9-16bd01f3aebb/icon.webp",
"email": "mike.james@example.com",
"forename": "Mike",
"surname": "James",
"department": "bae35c7a-a929-4465-b70f-03254b0774e0",
"create_timestamp": "2024-01-13T20:53:42Z",
"edit_timestamp": "2024-01-13T20:54:15.252Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/c53ce609-5775-4ab6-8995-c22c1d96f00b/icon.webp",
"email": "alfredo.fernandez@example.com",
"forename": "Alfredo",
"surname": "Fernandez",
"department": "a6517555-0bcc-4baa-8e2f-798916562b1c",
"create_timestamp": "2024-01-13T20:56:00Z",
"edit_timestamp": "2024-01-13T20:56:45.840Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "d84476db-f240-41ba-af9c-b23eba22894e",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/d84476db-f240-41ba-af9c-b23eba22894e/icon.webp",
"email": "donald.campbell@example.com",
"forename": "Donald",
"surname": "Campbell",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T21:03:17Z",
"edit_timestamp": "2024-01-13T21:03:35.083Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "de7ef0f6-dbe8-49be-864c-19a94465c68a",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/de7ef0f6-dbe8-49be-864c-19a94465c68a/icon.webp",
"email": "daryl.shaw@example.com",
"forename": "Daryl",
"surname": "Shaw",
"department": "a6517555-0bcc-4baa-8e2f-798916562b1c",
"create_timestamp": "2024-01-13T20:31:45Z",
"edit_timestamp": "2024-01-13T20:32:34.293Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "e802e8b4-d232-4b4a-b9c9-3e0e091145e6",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/e802e8b4-d232-4b4a-b9c9-3e0e091145e6/icon.webp",
"email": "larry.fernandez@example.com",
"forename": "Larry",
"surname": "Fernandez",
"department": "c2bbabd7-05ac-4bc8-97a3-15bafdb478d9",
"create_timestamp": "2024-01-13T21:01:07Z",
"edit_timestamp": "2024-01-13T21:01:27.482Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "f1d088f2-f4ae-4fdd-bfea-4bb54d36f3d2",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/f1d088f2-f4ae-4fdd-bfea-4bb54d36f3d2/icon.webp",
"email": "lydia.pearson@example.com",
"forename": "Lydia",
"surname": "Pearson",
"department": "85b46ae8-0a19-48b7-8a21-a01abd78a470",
"create_timestamp": "2024-01-13T20:59:24Z",
"edit_timestamp": "2024-01-13T21:00:16.950Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
},
{
"model": "authentication.user",
"pk": "fdaa7ec8-64f7-4496-b286-ba0a971d4290",
"fields": {
"password": "pbkdf2_sha256$260000$h4zLYmIeMQgJ0ko41i6dxo$Gwe0TV75ibdJTtqdTOOs5ucOEhA9DUM/bwxjagVhKKg=",
"last_login": null,
"icon": "users/fdaa7ec8-64f7-4496-b286-ba0a971d4290/icon.webp",
"email": "lena.daniels@example.com",
"forename": "Lena",
"surname": "Daniels",
"department": "a6517555-0bcc-4baa-8e2f-798916562b1c",
"create_timestamp": "2024-01-13T20:55:15Z",
"edit_timestamp": "2024-01-13T20:56:00.068Z",
"is_active": true,
"is_staff": false,
"is_superuser": false,
"groups": [],
"user_permissions": []
}
}
]

View File

@ -1,4 +1,4 @@
# Generated by Django 3.2.16 on 2024-01-07 18:45
# Generated by Django 3.2.16 on 2024-01-22 14:39
import apps.authentication.models
from django.db import migrations, models
@ -19,9 +19,11 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Department',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.CharField(max_length=150)),
('icon', models.CharField(blank=True, max_length=32, null=True)),
('colour', models.CharField(max_length=7)),
('backgroundcolour', models.CharField(max_length=7)),
('order', models.PositiveIntegerField(default=0)),
],
),
migrations.CreateModel(
@ -29,8 +31,8 @@ class Migration(migrations.Migration):
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('icon', models.ImageField(default='../static/assets/images/defaultuser.webp', storage=apps.authentication.models.OverwriteStorage(), upload_to=apps.authentication.models.IconPathGenerator(), verbose_name='profile picture')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('icon', models.ImageField(default='../static/images/defaultuser.webp', storage=apps.authentication.models.OverwriteStorage(), upload_to=apps.authentication.models.IconPathGenerator(), verbose_name='profile picture')),
('email', models.EmailField(error_messages={'unique': 'A user with this email address already exists.'}, max_length=254, unique=True, verbose_name='email address')),
('forename', models.CharField(help_text='This should be your real first name.', max_length=150, verbose_name='first name')),
('surname', models.CharField(help_text='This should be your real last name.', max_length=150, verbose_name='last name')),

View File

@ -1,35 +0,0 @@
# Generated by Django 3.2.16 on 2024-01-05 10:44
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
from django.apps import apps
import uuid
def create_departments(app, schema_editor):
Department = apps.get_model("authentication", "Department")
data = [
{"title": "Development", "icon": None},
{"title": "Sales", "icon": None},
{"title": "Marketing", "icon": None},
{"title": "Management", "icon": None},
{"title": "Business Strategy", "icon": None},
]
for item in data:
priority = Department.objects.create(title=item["title"], icon=item["icon"])
class Migration(migrations.Migration):
dependencies = [
("authentication", "0001_initial"),
]
operations = [
migrations.RunPython(create_departments)
]

View File

@ -1,9 +1,10 @@
# -*- encoding: utf-8 -*-
import uuid
from uuid import uuid4
import os
from django.db import models
from django.conf import settings
from django.utils import timezone
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.utils.translation import gettext_lazy as _
@ -23,24 +24,20 @@ class OverwriteStorage(FileSystemStorage):
@deconstructible
class IconPathGenerator:
def __call__(self, instance, filename: str) -> str:
return os.path.join("users", str(instance.id), "icon.webp")
return os.path.join("users", str(instance.uuid), "icon.webp")
class Department(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
title = models.CharField(max_length=150)
icon = models.CharField(max_length=32, null=True, blank=True)
colour = models.CharField(max_length=7)
backgroundcolour = models.CharField(max_length=7)
order = models.PositiveIntegerField(default=0, blank=False, null=False)
def __str__(self):
return self.title
def serialize(self) -> dict:
return {
"title": self.title,
"icon": self.icon
}
class UserManager(BaseUserManager):
@ -70,12 +67,12 @@ class UserManager(BaseUserManager):
class User(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
icon = models.ImageField(
_("profile picture"),
upload_to=IconPathGenerator(),
default="../static/assets/images/defaultuser.webp",
default="../static/images/defaultuser.webp", # Path starts from media dir
storage=OverwriteStorage()
)
email = models.EmailField(
@ -145,7 +142,7 @@ class User(AbstractBaseUser, PermissionsMixin):
verbose_name_plural = _('users')
def __str__(self):
return f"{self.id}{self.email}{self.formal_fullname}"
return f"{self.uuid}{self.email}{self.formal_fullname}"
def save(self, *args, **kwargs):
self.edit_timestamp = timezone.now()
@ -162,17 +159,3 @@ class User(AbstractBaseUser, PermissionsMixin):
@property
def formal_fullname(self) -> str:
return f"{self.surname}, {self.forename}"
def serialize(self) -> dict:
department = self.department.serialize() if self.department else None
return {
"id": self.id,
"icon": self.icon.url,
"email": self.email,
"forename": self.forename,
"surname": self.surname,
"department": department,
"create_timestamp": self.create_timestamp,
"edit_timestamp": self.edit_timestamp
}

View File

@ -1,9 +1,47 @@
# -*- encoding: utf-8 -*-
from collections.abc import Callable
from typing import Any
from django.contrib import admin
from .models import Ticket, TicketPriority, TicketTag
admin.site.register(Ticket)
admin.site.register(TicketPriority)
admin.site.register(TicketTag)
class TicketAdmin(admin.ModelAdmin):
list_display = ["uuid", "title", "get_author_name", "get_priority_title", "get_tags_count"]
@admin.display(description="Author", ordering="author__name")
def get_author_name(self, obj):
return obj.author.fullname if obj.author else None
@admin.display(description="Priority", ordering="priority__title")
def get_priority_title(self, obj):
return obj.priority.title if obj.priority else None
@admin.display(description="Tags")
def get_tags_count(self, obj):
return obj.tags.count()
class TicketPriorityAdmin(admin.ModelAdmin):
list_display = ["uuid", "title", "colour", "backgroundcolour", "order", "get_used_count"]
@admin.display(description="Used by")
def get_used_count(self, obj):
return Ticket.objects.filter(priority=obj).count()
class TicketTagAdmin(admin.ModelAdmin):
list_display = ["uuid", "title", "colour", "backgroundcolour", "order", "get_used_count"]
@admin.display(description="Used by")
def get_used_count(self, obj):
return Ticket.objects.filter(tags__in=[obj]).count()
admin.site.register(Ticket, TicketAdmin)
admin.site.register(TicketPriority, TicketPriorityAdmin)
admin.site.register(TicketTag, TicketTagAdmin)

View File

@ -1,7 +1,4 @@
# -*- encoding: utf-8 -*-
"""
Copyright (c) 2019 - present AppSeed.us
"""
from django.apps import AppConfig

View File

@ -0,0 +1,634 @@
[
{
"model": "home.ticket",
"pk": "0725deef-d48c-4e38-814a-ae35fcbad152",
"fields": {
"title": "Software Compatibility Challenge",
"description": "Attempting to integrate a new software tool into our existing workflow, but facing compatibility challenges with other applications. Providing a detailed overview of the software stack and the specific issues encountered during integration attempts. Seeking expert guidance to resolve compatibility issues and ensure smooth workflow integration.",
"author": "21b457a1-b64a-4499-8c53-0e2f3b42fe3c",
"priority": "0ebc194c-b856-4e4f-9def-cd190d1e8d43",
"create_timestamp": "2024-01-09T00:11:40Z",
"edit_timestamp": "2024-01-03T15:53:48.787Z",
"tags": [
"0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b",
"72fb255c-132f-4124-802d-f4c051620540",
"cc473838-acaf-43f9-a601-dc4ab1f9026c"
]
}
},
{
"model": "home.ticket",
"pk": "1e648e67-ddf6-4007-bb68-4a863961a827",
"fields": {
"title": "Security Patch Installation",
"description": "There's a critical security patch that needs to be installed on all workstations to address recent vulnerabilities. Attempted to deploy the patch, but facing challenges on certain machines. Need high-priority assistance to ensure all systems are promptly updated for enhanced security measures.",
"author": "248bc1ef-df52-445e-847c-e370dccf436a",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"create_timestamp": "2024-01-09T00:09:20Z",
"edit_timestamp": "2023-06-29T15:53:31.535Z",
"tags": [
"0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b",
"72fb255c-132f-4124-802d-f4c051620540",
"cc473838-acaf-43f9-a601-dc4ab1f9026c"
]
}
},
{
"model": "home.ticket",
"pk": "433200a2-3a62-4e81-86cc-cd5e31fecd8b",
"fields": {
"title": "Network Connectivity Issue",
"description": "Experiencing intermittent connection drops in the office. Several users affected. Need urgent assistance to resolve the issue.",
"author": "9f469a37-4d8d-4bd0-ba4e-16b07549f42a",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"create_timestamp": "2024-01-09T00:05:54Z",
"edit_timestamp": "2024-01-13T15:53:18.994Z",
"tags": [
"0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b",
"28b160b5-5c8b-43a5-84d1-4179bde87e6f",
"72fb255c-132f-4124-802d-f4c051620540"
]
}
},
{
"model": "home.ticket",
"pk": "4b893b70-8fce-49a1-b546-d1960ef97f9c",
"fields": {
"title": "New Software Request",
"description": "Requesting the installation of a new software tool for project management. Providing specific details on the software requirements and its relevance to project workflows. Seeking guidance on the installation process and any potential compatibility issues. Your assistance in this matter is highly appreciated.",
"author": "77b65a5b-2399-47cd-a34c-62454309a51a",
"priority": "0ebc194c-b856-4e4f-9def-cd190d1e8d43",
"create_timestamp": "2024-01-09T00:09:59Z",
"edit_timestamp": "2024-01-16T15:52:55.759Z",
"tags": [
"72fb255c-132f-4124-802d-f4c051620540",
"cc473838-acaf-43f9-a601-dc4ab1f9026c",
"dc4dc2d5-2784-4726-8a4e-99df35a143d2"
]
}
},
{
"model": "home.ticket",
"pk": "587b35a3-1b47-4716-99bf-ebe22da283a1",
"fields": {
"title": "Database Query Performance",
"description": "Users are reporting slow response times when querying the database. This issue is impacting productivity across multiple teams. Detailed logs and steps taken to troubleshoot are provided within the description. Urgently seeking assistance to optimize database performance and resolve the slowdown.",
"author": "811ad22b-5153-421f-bb84-6addcb8de570",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"create_timestamp": "2024-01-09T00:08:32Z",
"edit_timestamp": "2024-01-16T15:52:45.715Z",
"tags": [
"0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b",
"28b160b5-5c8b-43a5-84d1-4179bde87e6f",
"72fb255c-132f-4124-802d-f4c051620540",
"cc473838-acaf-43f9-a601-dc4ab1f9026c",
"e8c7e801-57c3-4699-a4f4-4308fb489f60"
]
}
},
{
"model": "home.ticket",
"pk": "5f39644f-0356-4818-aa07-ee6f07f36553",
"fields": {
"title": "Hardware Malfunction",
"description": "The printer on the third floor is not responding. Checked cables and power source, but issue persists. Need assistance to fix the hardware problem.",
"author": "f1d088f2-f4ae-4fdd-bfea-4bb54d36f3d2",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"create_timestamp": "2024-01-09T00:06:58Z",
"edit_timestamp": "2024-01-16T15:52:35.868Z",
"tags": [
"0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b",
"72fb255c-132f-4124-802d-f4c051620540",
"e8c7e801-57c3-4699-a4f4-4308fb489f60"
]
}
},
{
"model": "home.ticket",
"pk": "68fa33a7-9ac0-4612-8d92-71201c1dd5d5",
"fields": {
"title": "Printer Configuration Error",
"description": "Encountering issues with configuring a new printer for the design team. Detailed steps taken to set up the printer and the specific error messages received are provided. Urgently seeking assistance to ensure the printer is operational and meets the team's printing requirements.",
"author": "0b437298-7c00-47fa-a54f-d3cfdf39a0c3",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"create_timestamp": "2024-01-09T00:10:25Z",
"edit_timestamp": "2024-01-16T15:52:25.424Z",
"tags": [
"0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b",
"72fb255c-132f-4124-802d-f4c051620540",
"e8c7e801-57c3-4699-a4f4-4308fb489f60"
]
}
},
{
"model": "home.ticket",
"pk": "9f72bd78-2d1f-4fd6-89fc-d9ecf8c69b3c",
"fields": {
"title": "VPN Connection Issue",
"description": "Remote team members are encountering difficulties establishing a VPN connection. This is hindering their ability to access essential resources. Providing detailed information on the error messages received and troubleshooting steps taken so far. Requesting immediate attention to restore seamless VPN functionality.",
"author": "8b504054-ab46-4866-8a7e-46aaab29e54b",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"create_timestamp": "2024-01-09T00:09:39Z",
"edit_timestamp": "2024-01-16T15:52:14.891Z",
"tags": [
"0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b",
"28b160b5-5c8b-43a5-84d1-4179bde87e6f",
"72fb255c-132f-4124-802d-f4c051620540"
]
}
},
{
"model": "home.ticket",
"pk": "b0142315-b5c4-46a9-b02e-bdfdd6dffc37",
"fields": {
"title": "IT Training Request",
"description": "Requesting IT training sessions for the marketing team to enhance their proficiency in utilizing specific software tools. Providing a detailed outline of the desired training topics and the anticipated benefits for the team. Seeking assistance in scheduling and conducting the training sessions.",
"author": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"priority": "0ebc194c-b856-4e4f-9def-cd190d1e8d43",
"create_timestamp": "2024-01-09T00:12:17Z",
"edit_timestamp": "2024-01-16T15:52:02.743Z",
"tags": [
"72fb255c-132f-4124-802d-f4c051620540",
"cc473838-acaf-43f9-a601-dc4ab1f9026c"
]
}
},
{
"model": "home.ticket",
"pk": "c470a8b3-ca54-4324-abdf-1cfa4cdf70d6",
"fields": {
"title": "Email Configuration Assistance",
"description": "Need assistance in configuring email settings for a new team member. Providing the email client details and steps taken so far. Urgently seeking guidance to ensure the seamless setup of email accounts and communication channels for the new team member.",
"author": "94ca520d-4f5a-49c6-ab2b-9fbb362c51d9",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"create_timestamp": "2024-01-09T00:11:57Z",
"edit_timestamp": "2024-01-16T15:51:45.540Z",
"tags": [
"0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b",
"72fb255c-132f-4124-802d-f4c051620540"
]
}
},
{
"model": "home.ticket",
"pk": "dd5764ad-e765-4f9b-9a9b-a3a27e60c6dd",
"fields": {
"title": "General Inquiry",
"description": "Have a question regarding the new IT policies. Need clarification on specific guidelines. Please provide assistance at your earliest convenience.",
"author": "48ec50cd-d89b-41d7-bcb0-92612f0cf104",
"priority": "0ebc194c-b856-4e4f-9def-cd190d1e8d43",
"create_timestamp": "2024-01-09T00:07:26Z",
"edit_timestamp": "2024-01-16T15:51:38.099Z",
"tags": [
"dc4dc2d5-2784-4726-8a4e-99df35a143d2"
]
}
},
{
"model": "home.ticket",
"pk": "fd8a4ad9-04fb-4a0c-beac-e60e0739e881",
"fields": {
"title": "Software Update Problem",
"description": "Unable to install the latest software update on my workstation. Getting error code XYZ. Detailed steps attempted are listed in the description.",
"author": "c1984735-fdca-488b-b4e9-16bd01f3aebb",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"create_timestamp": "2024-01-09T00:06:32Z",
"edit_timestamp": "2024-01-16T15:51:31.695Z",
"tags": [
"0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b",
"72fb255c-132f-4124-802d-f4c051620540",
"cc473838-acaf-43f9-a601-dc4ab1f9026c"
]
}
},
{
"model": "home.ticket",
"fields": {
"title": "System Performance Analysis",
"description": "Experiencing slow system performance during peak hours. This issue is impacting our team's productivity. We need a thorough analysis of the system's performance bottlenecks and recommendations for optimization.",
"author": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"tags": ["0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b", "e8c7e801-57c3-4699-a4f4-4308fb489f60"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Software Update Request",
"description": "We need assistance with updating our project management software to the latest version. There have been reported bugs in the current version impacting our workflow. Looking for guidance on a smooth update process and resolving any compatibility issues.",
"author": "94ca520d-4f5a-49c6-ab2b-9fbb362c51d9",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Network Connection Dropping",
"description": "Experiencing intermittent network connectivity issues across multiple devices. The problem seems to be affecting both wired and wireless connections. Urgently seeking assistance to diagnose and resolve the connectivity issues to ensure smooth workflow.",
"author": "811ad22b-5153-421f-bb84-6addcb8de570",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b", "28b160b5-5c8b-43a5-84d1-4179bde87e6f"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Database Performance Tuning",
"description": "The database queries are taking longer than usual, impacting overall system performance. Seeking expertise in optimizing database performance to enhance system responsiveness.",
"author": "4965745e-b82a-4496-80e1-055217a780b0",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Security Audit Request",
"description": "Concerns about the security of our system. Requesting a comprehensive security audit to identify vulnerabilities and implement necessary measures for a secure environment.",
"author": "855d09bb-5919-4dff-8652-50aa1fb81b54",
"priority": "0ebc194c-b856-4e4f-9def-cd190d1e8d43",
"tags": ["0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b", "e8c7e801-57c3-4699-a4f4-4308fb489f60"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Software Training Request",
"description": "New software tools have been introduced, and the team requires training to use them efficiently. Looking for guidance on available training resources and scheduling training sessions.",
"author": "de7ef0f6-dbe8-49be-864c-19a94465c68a",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Email Configuration Issue",
"description": "Unable to send or receive emails through the company email system. Requesting assistance to troubleshoot and resolve the email configuration issue to ensure uninterrupted communication.",
"author": "811ad22b-5153-421f-bb84-6addcb8de570",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b", "28b160b5-5c8b-43a5-84d1-4179bde87e6f"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Server Downtime Investigation",
"description": "Frequent server downtime reported by users. Seeking an in-depth investigation to identify the root cause of the downtime and implement measures to prevent future occurrences.",
"author": "be2acfc8-7623-4f5f-864b-4163e07b881d",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"tags": ["0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b", "28b160b5-5c8b-43a5-84d1-4179bde87e6f"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Software License Renewal",
"description": "Our software licenses are expiring soon. Seeking assistance in renewing licenses and ensuring that all software tools are properly licensed to avoid any legal issues.",
"author": "811ad22b-5153-421f-bb84-6addcb8de570",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Hardware Upgrade Request",
"description": "The team is experiencing performance issues with outdated hardware. Requesting approval and assistance for a hardware upgrade to improve overall system performance.",
"author": "855d09bb-5919-4dff-8652-50aa1fb81b54",
"priority": "0ebc194c-b856-4e4f-9def-cd190d1e8d43",
"tags": ["e8c7e801-57c3-4699-a4f4-4308fb489f60", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Cleanliness Emergency - Bathroom Cleanup Needed",
"description": "Dear IT Support Team,<p>I hope this message finds you well. I am writing to bring to your attention a critical issue in the office bathrooms that requires immediate attention.</p><p>It seems there's a severe cleanliness problem, with shit found on the walls and even on the fucking ceiling! This is highly unhygienic and unacceptable, creating an uncomfortable environment for everyone using the facilities.</p><p>I understand that maintenance and cleanliness are not directly under the IT department's purview, but it would be greatly appreciated if you could liaise with the relevant personnel or department to ensure a swift resolution to this matter.</p><p>Your prompt action on this issue is crucial to maintain a healthy and professional working environment. Thank you for your attention to this urgent matter.</p><p>Best regards</p>",
"author": "d84476db-f240-41ba-af9c-b23eba22894e",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "User Access Permission Issue",
"description": "Some team members are facing issues accessing specific files and folders. Seeking assistance in resolving user access permission problems and ensuring that everyone has the required access.",
"author": "855d09bb-5919-4dff-8652-50aa1fb81b54",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b", "e8c7e801-57c3-4699-a4f4-4308fb489f60"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "VPN Not Working Again",
"description": "Unable to establish a stable VPN connection for remote team members. This is hindering collaboration and access to shared resources. Seeking assistance in resolving VPN connectivity problems.",
"author": "9cc67b83-2ccc-454e-be4a-9c772cb23d87",
"priority": "0ebc194c-b856-4e4f-9def-cd190d1e8d43",
"tags": ["0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b", "28b160b5-5c8b-43a5-84d1-4179bde87e6f"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Web Application Error",
"description": "Users are encountering frequent errors while using the web application. These errors disrupt the user experience. Seeking assistance in identifying and resolving issues within the web application.",
"author": "9f469a37-4d8d-4bd0-ba4e-16b07549f42a",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Collaboration Tool Setup",
"description": "Requesting assistance in setting up a new collaboration tool for team communication and project management. Need guidance on the best practices for efficient tool utilization.",
"author": "d84476db-f240-41ba-af9c-b23eba22894e",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Printer Connectivity Issue",
"description": "Facing difficulties in connecting to the office printer. This issue is impacting the ability to print important documents. Seeking assistance to resolve printer connectivity problems.",
"author": "855d09bb-5919-4dff-8652-50aa1fb81b54",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b", "28b160b5-5c8b-43a5-84d1-4179bde87e6f"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Mobile Device Configuration",
"description": "Team members are experiencing issues with configuring their mobile devices to access company resources. Seeking assistance in setting up mobile devices for secure and seamless access.",
"author": "855d09bb-5919-4dff-8652-50aa1fb81b54",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Software License Audit",
"description": "Conducting a comprehensive audit of software licenses to ensure compliance with legal requirements. Seeking guidance on the auditing process and recommendations for license optimization.",
"author": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "e8c7e801-57c3-4699-a4f4-4308fb489f60"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Remote Desktop Connection Issue",
"description": "Unable to establish a stable remote desktop connection. This is affecting remote team collaboration. Seeking assistance in troubleshooting and resolving remote desktop connectivity problems.",
"author": "9cc67b83-2ccc-454e-be4a-9c772cb23d87",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b", "28b160b5-5c8b-43a5-84d1-4179bde87e6f"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Software Deployment Request",
"description": "Requesting assistance in deploying new software across the team. Need guidance on the deployment process, best practices, and ensuring a smooth transition for all users.",
"author": "79ab62dc-878e-4563-8b82-1537de6f4a3c",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Audio-Visual Setup Assistance",
"description": "Setting up an audio-visual system for an upcoming presentation. Seeking assistance in configuring and optimizing audio and visual components for a successful presentation.",
"author": "d84476db-f240-41ba-af9c-b23eba22894e",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Data Migration Assistance",
"description": "Planning to migrate data to a new server. Need guidance on the data migration process, ensuring data integrity, and minimizing downtime during the migration.",
"author": "9cc67b83-2ccc-454e-be4a-9c772cb23d87",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Training Room Setup",
"description": "Preparing for a team training session and need assistance in setting up the training room. Seeking guidance on audio-visual equipment, seating arrangements, and logistics.",
"author": "855d09bb-5919-4dff-8652-50aa1fb81b54",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"tags": ["e8c7e801-57c3-4699-a4f4-4308fb489f60", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Software Bug Report",
"description": "Users have encountered unexpected behavior in the software. Submitting a bug report to address and resolve the issues. Including detailed information on the encountered bugs.",
"author": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b", "e8c7e801-57c3-4699-a4f4-4308fb489f60"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Server Room Temperature Monitoring",
"description": "Concerns about the temperature in the server room. Seeking assistance in setting up temperature monitoring systems to ensure optimal conditions for server operation.",
"author": "79ab62dc-878e-4563-8b82-1537de6f4a3c",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["e8c7e801-57c3-4699-a4f4-4308fb489f60", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Software Update Request",
"description": "Requesting the latest software updates for critical applications. Seeking guidance on the update process, potential impacts, and ensuring minimal disruption to ongoing projects.",
"author": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "e8c7e801-57c3-4699-a4f4-4308fb489f60"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Software Update Request",
"description": "Requesting the latest software updates for critical applications. Seeking guidance on the update process, potential impacts, and ensuring minimal disruption to ongoing projects.",
"author": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "e8c7e801-57c3-4699-a4f4-4308fb489f60"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Smart Office IoT Implementation",
"description": "Exploring the implementation of IoT devices for a smart office environment. Seeking guidance on selecting suitable IoT devices, ensuring security, and optimizing the office environment.",
"author": "79ab62dc-878e-4563-8b82-1537de6f4a3c",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"tags": ["e8c7e801-57c3-4699-a4f4-4308fb489f60", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Network Performance Optimization",
"description": "Experiencing slow network performance affecting daily operations. Seeking assistance in optimizing network settings, identifying bottlenecks, and ensuring efficient data transfer.",
"author": "9cc67b83-2ccc-454e-be4a-9c772cb23d87",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["28b160b5-5c8b-43a5-84d1-4179bde87e6f", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Security Vulnerability Report",
"description": "Identified potential security vulnerabilities in the system. Submitting a report to address and mitigate security risks. Including details on vulnerabilities and suggested solutions.",
"author": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"tags": ["0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b", "cc473838-acaf-43f9-a601-dc4ab1f9026c"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Password Reset Assistance",
"description": "Users experiencing difficulty in resetting their passwords. Seeking assistance in providing a secure and efficient process for password resets to ensure smooth access to accounts.",
"author": "855d09bb-5919-4dff-8652-50aa1fb81b54",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Software Training Request",
"description": "Team members requesting training sessions for newly implemented software. Seeking guidance on organizing training sessions, providing relevant resources, and ensuring a smooth learning experience.",
"author": "d84476db-f240-41ba-af9c-b23eba22894e",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Server Backup Configuration",
"description": "Reviewing and configuring server backup processes. Seeking assistance in ensuring regular and secure backups to prevent data loss and ensure business continuity.",
"author": "79ab62dc-878e-4563-8b82-1537de6f4a3c",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["e8c7e801-57c3-4699-a4f4-4308fb489f60", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Malware Incident - Files Missing, Desktop Black",
"description": "Employee reported a malware incident on their workstation. They've noticed that files are missing, and the desktop background has turned black. Immediate assistance is needed to assess the extent of the malware, recover any lost files, and ensure the workstation is secure. Requesting urgent attention to mitigate potential data loss and security risks.",
"author": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "New Hardware Request - Platinum Laptop Edition",
"description": "<p>Hello IT team, I hope this message finds you well. I'm writing to urgently request the latest Platinum Laptop Edition, which, according to my meticulous research (conducted during a particularly productive coffee break), is the only device that can unleash my full potential.</p><p>You see, the standard-issue laptops are simply holding me back from achieving peak productivity and enlightenment.</p><p>Now, you might wonder why I need the Platinum Laptop Edition. Well, it's not just a want; it's a dire necessity for my groundbreaking work in...uh, staring at the screensaver during brainstorming sessions. The sleek design and extra shine are crucial for creative inspiration.</p><p>Moreover, the built-in diamond-encrusted keyboard will undoubtedly elevate my typing skills to new heights. I firmly believe that the gentle caress of precious gems against my fingertips will unlock hidden depths of wisdom and efficiency.</p><p>In summary, the Platinum Laptop Edition is not just a laptop; it's the key to unlocking my true potential. I understand this may seem like an unnecessary request, but trust me, the future of innovation rests upon the shoulders of this laptop. Your prompt approval will undoubtedly lead to a revolution in... something.</p><p>Looking forward to your understanding and swift action on this matter. Together, we can usher in a new era of productivity and undeniable opulence.</p><p>Best regards, [Employee's Name]</p>",
"author": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Software License Renewal",
"description": "Notifying the IT department about an upcoming software license expiration. Requesting assistance in renewing the license to avoid disruptions in workflow. Please provide guidance on the renewal process.",
"author": "79ab62dc-878e-4563-8b82-1537de6f4a3c",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "e8c7e801-57c3-4699-a4f4-4308fb489f60"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Mobile Device Setup",
"description": "Requesting assistance in setting up a new mobile device for work purposes. Need guidance on configuring email, security settings, and ensuring seamless synchronization with work applications.",
"author": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Meeting Room AV Setup",
"description": "Organizing an important presentation in the meeting room. Requesting assistance in setting up audiovisual equipment, ensuring proper connectivity, and conducting a brief test before the scheduled meeting.",
"author": "855d09bb-5919-4dff-8652-50aa1fb81b54",
"priority": "a680328f-0680-456c-8e26-f594e05989ad",
"tags": ["28b160b5-5c8b-43a5-84d1-4179bde87e6f", "e8c7e801-57c3-4699-a4f4-4308fb489f60"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Internet Connectivity Issues",
"description": "Experiencing intermittent internet connectivity issues. Need assistance in diagnosing the problem, identifying potential sources of disruption, and restoring stable internet access for uninterrupted work.",
"author": "9cc67b83-2ccc-454e-be4a-9c772cb23d87",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["28b160b5-5c8b-43a5-84d1-4179bde87e6f", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Employee Onboarding IT Setup",
"description": "New employee joining the team. Requesting IT assistance to set up their workstation with the necessary software, access permissions, and email configuration. Timely support is appreciated to ensure a smooth onboarding process.",
"author": "d84476db-f240-41ba-af9c-b23eba22894e",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "File Access Permission Issue",
"description": "Encountering difficulties accessing specific files. Requesting IT intervention to review and adjust file permissions, ensuring the necessary access for smooth workflow. Urgent attention is appreciated.",
"author": "c53ce609-5775-4ab6-8995-c22c1d96f00b",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Server Storage Expansion Request",
"description": "Experiencing limitations in server storage capacity. Requesting IT support to expand storage capacity, ensuring sufficient space for ongoing projects and data. Timely action is crucial to avoid disruptions.",
"author": "855d09bb-5919-4dff-8652-50aa1fb81b54",
"priority": "d140a5be-cf24-4250-8b38-31338e69dffd",
"tags": ["e8c7e801-57c3-4699-a4f4-4308fb489f60", "dc4dc2d5-2784-4726-8a4e-99df35a143d2"]
}
},
{
"model": "home.ticket",
"fields": {
"title": "Smartphone Email Sync Issue",
"description": "Encountering synchronization issues with work email on the smartphone. Requesting IT assistance to troubleshoot and resolve the problem, ensuring seamless access to emails on the go.",
"author": "9cc67b83-2ccc-454e-be4a-9c772cb23d87",
"priority": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"tags": ["cc473838-acaf-43f9-a601-dc4ab1f9026c", "28b160b5-5c8b-43a5-84d1-4179bde87e6f"]
}
}
]

View File

@ -0,0 +1,38 @@
[
{
"model": "home.ticketpriority",
"pk": "d140a5be-cf24-4250-8b38-31338e69dffd",
"fields": {
"title": "Urgent",
"colour": "#c62828",
"backgroundcolour": "#ffcdd2"
}
},
{
"model": "home.ticketpriority",
"pk": "a680328f-0680-456c-8e26-f594e05989ad",
"fields": {
"title": "High",
"colour": "#ef6c00",
"backgroundcolour": "#ffe0b2"
}
},
{
"model": "home.ticketpriority",
"pk": "e79687c6-9054-4706-b9a2-34afccfaa7c8",
"fields": {
"title": "Normal",
"colour": "#2e7d32",
"backgroundcolour": "#c8e6c9"
}
},
{
"model": "home.ticketpriority",
"pk": "0ebc194c-b856-4e4f-9def-cd190d1e8d43",
"fields": {
"title": "Low",
"colour": "#388e3c",
"backgroundcolour": "#e8f5e9"
}
}
]

View File

@ -0,0 +1,56 @@
[
{
"model": "home.tickettag",
"pk": "0ac68e5d-9000-4fcb-bb44-40b1b0faaa2b",
"fields": {
"title": "Issue",
"colour": "#e91e63",
"backgroundcolour": "#fce4ec"
}
},
{
"model": "home.tickettag",
"pk": "28b160b5-5c8b-43a5-84d1-4179bde87e6f",
"fields": {
"title": "Network",
"colour": "#512da8",
"backgroundcolour": "#d1c4e9"
}
},
{
"model": "home.tickettag",
"pk": "72fb255c-132f-4124-802d-f4c051620540",
"fields": {
"title": "Requires Help",
"colour": "#388e3c",
"backgroundcolour": "#c8e6c9"
}
},
{
"model": "home.tickettag",
"pk": "cc473838-acaf-43f9-a601-dc4ab1f9026c",
"fields": {
"title": "Software",
"colour": "#f57c00",
"backgroundcolour": "#ffe0b2"
}
},
{
"model": "home.tickettag",
"pk": "dc4dc2d5-2784-4726-8a4e-99df35a143d2",
"fields": {
"title": "Question",
"colour": "#00796b",
"backgroundcolour": "#b2dfdb"
}
},
{
"model": "home.tickettag",
"pk": "e8c7e801-57c3-4699-a4f4-4308fb489f60",
"fields": {
"title": "Hardware",
"colour": "#303f9f",
"backgroundcolour": "#c5cae9"
}
}
]

View File

@ -0,0 +1,28 @@
# Generated by Django 3.2.16 on 2024-01-12 16:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='ticket',
old_name='id',
new_name='uuid',
),
migrations.RenameField(
model_name='ticketpriority',
old_name='id',
new_name='uuid',
),
migrations.RenameField(
model_name='tickettag',
old_name='id',
new_name='uuid',
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.16 on 2024-01-22 13:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0002_auto_20240112_1604'),
]
operations = [
migrations.AddField(
model_name='ticketpriority',
name='order',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='tickettag',
name='order',
field=models.PositiveIntegerField(default=0),
),
]

View File

@ -1,34 +0,0 @@
# Generated by Django 3.2.16 on 2024-01-05 10:44
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
from django.apps import apps
import uuid
def create_priorities(app, schema_editor):
TicketPriority = apps.get_model("home", "TicketPriority")
data = [
{"title": "Urgent", "colour": "#FFFFFF", "backgroundcolour": "#FF4D4D"},
{"title": "High", "colour": "#FFFFFF", "backgroundcolour": "#FF884D"},
{"title": "Normal", "colour": "#FFFFFF", "backgroundcolour": "#66CC66"},
{"title": "Low", "colour": "#FFFFFF", "backgroundcolour": "#66DD66"}
]
for item in data:
priority = TicketPriority.objects.create(title=item["title"], colour=item["colour"], backgroundcolour=item["backgroundcolour"])
class Migration(migrations.Migration):
dependencies = [
("home", "0001_initial"),
]
operations = [
migrations.RunPython(create_priorities)
]

View File

@ -1,36 +0,0 @@
# Generated by Django 3.2.16 on 2024-01-05 10:44
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
from django.apps import apps
import uuid
def create_tags(app, schema_editor):
TicketTag = apps.get_model("home", "TicketTag")
data = [
{"title": "Network", "colour": "#000000", "backgroundcolour": "#BFD3C1"},
{"title": "Software", "colour": "#000000", "backgroundcolour": "#FED8B1"},
{"title": "Hardware", "colour": "#FFFFFF", "backgroundcolour": "#CCCCCC"},
{"title": "Question", "colour": "#000000", "backgroundcolour": "#FFFCB1"},
{"title": "Requires Help", "colour": "#000000", "backgroundcolour": "#B2E57C"},
{"title": "Issue", "colour": "#000000", "backgroundcolour": "#FFB2B2"}
]
for item in data:
priority = TicketTag.objects.create(title=item["title"], colour=item["colour"], backgroundcolour=item["backgroundcolour"])
class Migration(migrations.Migration):
dependencies = [
("home", "default_priorities"),
]
operations = [
migrations.RunPython(create_tags)
]

View File

@ -1,7 +1,8 @@
# -*- encoding: utf-8 -*-
import uuid
import logging
import bleach
from uuid import uuid4
from datetime import timedelta, datetime
from django.db import models
@ -9,106 +10,121 @@ from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
log = logging.getLogger(__name__)
class TicketPriority(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
title = models.CharField(max_length=32)
colour = models.CharField(max_length=7)
backgroundcolour = models.CharField(max_length=7)
order = models.PositiveIntegerField(default=0, blank=False, null=False)
def __str__(self):
return self.title
def serialize(self) -> dict:
return {
"id": self.id,
"title": self.title,
"colour": self.colour,
"backgroundcolour": self.backgroundcolour
}
class TicketTag(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
title = models.CharField(max_length=32)
colour = models.CharField(max_length=7)
backgroundcolour = models.CharField(max_length=7)
order = models.PositiveIntegerField(default=0, blank=False, null=False)
def __str__(self):
return self.title
def serialize(self) -> dict:
return {
"id": self.id,
"title": self.title,
"colour": self.colour,
"backgroundcolour": self.backgroundcolour
}
class Ticket(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
"""Represents a Ticket used to communicate issues or questions."""
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
# Main Attributes
title = models.CharField(
_("title"),
verbose_name=_("title"),
help_text=_("An extremely short summary of the ticket subject."),
max_length=100,
help_text=_("An extremely short summary of the ticket subject.")
)
description = models.TextField(
_("description"),
verbose_name=_("description"),
help_text=_("Detailed description of the ticket subject."),
max_length=650,
help_text=_("Detailed description of the ticket subject.")
)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_("author"),
help_text=_("The creator of the ticket."),
on_delete=models.CASCADE,
help_text=_("The creator of the ticket.")
)
priority = models.ForeignKey(
TicketPriority,
verbose_name=_("priority"),
help_text=_("The importance level of this ticket."),
on_delete=models.CASCADE,
help_text=_("The importance level of this ticket.")
)
tags = models.ManyToManyField(
TicketTag,
verbose_name=_("tags"),
help_text=_("Categories of the ticket."),
blank=True,
help_text=_("Categories of the ticket.")
)
# Timestamps
create_timestamp = models.DateTimeField(
_("Creation Date"),
verbose_name=_("Creation Date"),
help_text=_("When the user was created."),
editable=True,
default=timezone.now,
help_text=_("When the user was created.")
)
edit_timestamp = models.DateTimeField(
_("Last Edited"),
verbose_name=_("Last Edited"),
help_text=_("When the user was last edited."),
editable=True,
default=timezone.now,
help_text=_("When the user was last edited.")
)
def __str__(self):
return f"#{self.id}{self.title}{f' {self.author.department.title}' if self.author.department else ''} {self.author.formal_fullname}"
return f"#{self.uuid}{self.title}{f' {self.author.department.title}' if self.author.department else ''} {self.author.formal_fullname}"
def clean_description(self):
"""Sanitise the description as it may contain some allowed HTML tags."""
cleaned_description = bleach.clean(
self.description,
tags=['b', 'i', 'u', 'p', 'br', 'a', 'h3', 'h4', 'h5', 'h6'],
tags=[
'b', 'i', 'u', 'p', 'br', 'a', 'h3', 'h4', 'h5', 'h6', 'strong',
'figure', 'table', 'tbody', 'tr', 'td'
],
attributes={'a': ['href', 'title']}
)
return cleaned_description
def short_description(self):
"""Provide a snippet of the description for presentation purposes."""
short_description = bleach.clean(self.description, tags=[])
if len(short_description) > 200:
return f"{short_description[:200]}..."
return short_description
def save(self, *args, **kwargs):
"""Override the save method to clean the description and apply timestamps."""
self.description = self.clean_description()
# self.edit_timestamp = timezone.now() ## TEMP COMMENT, UNCOMMENT LATER !!
now = timezone.now()
self.edit_timestamp = now
if self._state.adding:
self.create_timestamp = now
super().save(*args, **kwargs)
@property
@ -125,33 +141,44 @@ class Ticket(models.Model):
return self.create_timestamp != self.edit_timestamp
@property
def is_older_than_day(self) -> bool:
"""Returns boolean dependent on if `self.timestamp` is older than 24 hours.
def display_datetime(self) -> str:
"""Provides a human readable string representation of `self.timestamp` that should be displayed
to represent the ticket's age.
Returns
-------
bool
True if `self.timestamp` is older than 24 hours, False otherwise.
str
The string representation of `self.timestamp`.
"""
dayago = timezone.now() - timedelta(hours=24)
return self.timestamp <= dayago
difference = timezone.now() - self.timestamp
@property
def was_yesterday(self) -> bool:
"""_summary_
days = difference.days
hours = difference.total_seconds() // 3600
minutes = difference.total_seconds() // 60
seconds = difference.total_seconds()
Returns
-------
bool
_description_
"""
hours, minutes, seconds = map(int, (hours, minutes, seconds))
now = timezone.now()
midnight_today = now - timedelta(hours=now.hour, minutes=now.minute, seconds=now.second, microseconds=now.microsecond)
return self.timestamp < midnight_today
if seconds < 60:
value, unit = seconds, "second"
elif minutes < 60:
value, unit = minutes, "minute"
elif hours < 24:
value, unit = hours, "hour"
elif days < 7:
value, unit = days, "day"
else:
return self.timestamp.strftime("%Y-%m-%d")
if value > 1:
unit += "s"
return f"{value} {unit} ago"
@property
def timestamp(self) -> datetime:
@ -165,19 +192,3 @@ class Ticket(models.Model):
"""
return self.edit_timestamp if self.is_edited else self.create_timestamp
def serialize(self) -> dict:
return {
"id": self.id,
"title": self.title,
"description": self.description,
"author": self.author.serialize(),
"create_timestamp": self.create_timestamp,
"edit_timestamp": self.edit_timestamp,
"is_edited": self.is_edited,
"was_yesterday": self.was_yesterday,
"is_older_than_day": self.is_older_than_day,
"timestamp": self.timestamp,
"priority": self.priority.serialize(),
"tags": [tag.serialize() for tag in self.tags.all()]
}

View File

@ -1,25 +1,23 @@
# -*- encoding: utf-8 -*-
from django.urls import path, re_path, include
from django.urls import path, include
from django.shortcuts import redirect
from apps.home import views
from .views import DashboardView, TicketView
def reverse_to_index(reqeust):
return redirect("dashboard")
urlpatterns = [
# The home page
path('', views.index, name='home'),
# Custom Dashboard
path('dashboard/', views.dashboard, name="dashboard"),
path("", reverse_to_index, name="index"),
path('dashboard/', DashboardView.as_view(), name="dashboard"),
path('tickets/', include([
path('', views.tickets, name="tickets"),
path('', TicketView.as_view(), name="tickets"),
path('new/', views.new_ticket, name="ticket-new"),
path('get/', include([
path('one/', views.get_ticket, name="ticket-getone"),
path('many/', views.get_tickets, name="ticket-getmany"),
])),
])),
# Matches any html file
re_path(r'^.*\.*', views.pages, name='pages'),
# # Matches any html file
# re_path(r'^.*\.*', views.pages, name='pages'),
]

View File

@ -1,5 +1,6 @@
# -*- encoding: utf-8 -*-
import json
from datetime import timedelta, datetime
from django import template
@ -9,40 +10,47 @@ from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.template import loader
from django.shortcuts import render
from django.urls import reverse
from django.forms.models import model_to_dict
from django.contrib.auth import get_user_model
from django.views.generic import TemplateView
from django.utils.decorators import method_decorator
from apps.api.serializers import TicketSerializer
from ..authentication.models import Department
from .models import Ticket, TicketPriority, TicketTag
@login_required()
def dashboard(request):
return render(request, "home/dashboard.html")
class DashboardView(TemplateView):
template_name = "home/dashboard.html"
@method_decorator(login_required)
def get(self, request, *args, **kwargs):
return render(request, self.template_name)
@login_required()
def tickets(request):
tickets = Ticket.objects.all().order_by("-create_timestamp")
priorities = TicketPriority.objects.all()
tags = TicketTag.objects.all()
departments = Department.objects.all()
class TicketView(TemplateView):
template_name = "home/tickets.html"
context = {
"tickets": tickets,
"priorities": priorities,
"tags": tags,
"departments": departments,
"dayago": datetime.now() - timedelta(hours=24)
}
@method_decorator(login_required)
def get(self, request):
priorities = TicketPriority.objects.all().order_by("order")
tags = TicketTag.objects.all().order_by("order")
departments = Department.objects.all().order_by("order")
return render(request, "home/tickets.html", context)
context = {
"priorities": priorities,
"tags": tags,
"departments": departments
}
return render(request, "home/tickets.html", context)
@login_required
@require_POST
def get_ticket(request):
ticket = Ticket.objects.get(id=request.POST.get("ticket_id"))
data = {"ticket": ticket.serialize()}
ticket = Ticket.objects.get(uuid=request.POST.get("ticket_uuid"))
serializer = TicketSerializer(ticket)
data = {"ticket": serializer.data}
return JsonResponse(data)
@ -50,26 +58,83 @@ def get_ticket(request):
@require_POST
def get_tickets(request):
filters = dict(request.POST.get("filters", {}))
filters = json.loads(request.POST.get("filters", "{}"))
queryset = Ticket.objects.all()
tickets = Ticket.objects.filter(**filters).order_by("-create_timestamp")
data = {"tickets": [ticket.serialize() for ticket in tickets]}
for key, values in filters.items():
print(key, values)
for value in values:
if value == "all": continue # don't apply a filter if we want all
queryset = queryset.filter(**{key: [value]})
tickets = queryset.order_by("-create_timestamp")
serializer = TicketSerializer(tickets, many=True)
data = {"tickets": serializer.data}
return JsonResponse(data)
@login_required
@require_POST
def get_filter_counts(request):
priorities = TicketPriority.objects.all()
tags = TicketTag.objects.all()
departments = Department.objects.all()
tickets = Ticket.objects.all()
data = {
"priority_counts": {},
"tag_counts": {},
"department_counts": {},
"ticket_count": tickets.count()
}
for priority in priorities:
data["priority_counts"][str(priority.uuid)] = tickets.filter(priority=priority).count()
for tag in tags:
data["tag_counts"][str(tag.uuid)] = tickets.filter(tags__in=[tag]).count()
for department in departments:
data["department_counts"][str(department.uuid)] = tickets.filter(author__department=department).count()
return JsonResponse(data)
@login_required()
@require_POST
def new_ticket(request):
return JsonResponse({"placeholder": "nothing here yet"})
print(request.POST)
get = lambda key: request.POST.get(key)
getlist = lambda key: request.POST.getlist(key)
@login_required()
def index(request):
context = {'segment': 'index'}
title = get("title")
description = get("description")
author_id = get("author_id")
priority_id = get("priority_id")
tag_ids = getlist("tag_ids[]")
html_template = loader.get_template('home/index.html')
return HttpResponse(html_template.render(context, request))
User = get_user_model()
author = User.objects.get(id=author_id)
priority = TicketPriority.objects.get(id=priority_id)
tags = [
tag for tag in TicketTag.objects.filter(id__in=tag_ids)
]
ticket = Ticket.objects.create(
title=title,
description=description,
author=author,
priority=priority,
)
ticket.tags.set(tags)
return JsonResponse({"success": "ticket created successfully"})
@login_required()
@ -89,7 +154,6 @@ def pages(request):
return HttpResponse(html_template.render(context, request))
except template.TemplateDoesNotExist:
html_template = loader.get_template('home/page-404.html')
return HttpResponse(html_template.render(context, request))

10924
apps/static/5.2_bootstrap.css Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

45594
apps/static/css/adminator.css Normal file

File diff suppressed because it is too large Load Diff

12068
apps/static/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

5286
apps/static/css/colours.css Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3021
apps/static/css/fontawesome.css vendored Normal file

File diff suppressed because it is too large Load Diff

232
apps/static/css/index.css Normal file
View File

@ -0,0 +1,232 @@
@charset "UTF-8";
:root,
[data-bs-theme="light"] {
--border-colour: var(--bs-light-border-subtle);
}
[data-bs-theme="dark"] {
--border-colour: var(--bs-dark-border-subtle)
}
.select2-container {
z-index: 99999;
}
.select2-selection__choice {
display: flex;
align-items: center;
padding-left: 3px !important;
}
.select2-selection__choice > button {
border: none;
background-color: var(--bs-white-rgb);
}
.select2-results__option--selected { display: none; }
.ck-editor .ck.ck-editor__top .ck-toolbar {
border-top-left-radius: 0.375rem !important;
border-top-right-radius: 0.375rem !important;
}
.ck-editor .ck.ck-editor__main {
overflow: hidden;
}
.ck-editor .ck.ck-editor__main > div {
border-bottom-left-radius: 0.375rem !important;
border-bottom-right-radius: 0.375rem !important;
}
.ck-editor__editable[role="textbox"] {
/* editing area */
min-height: 200px;
max-height: 200px;
}
#ticketsContainer .loading .spinner-grow {
width: .8rem;
height: .8rem;
}
#ticketsContainer .loading .spinner-grow:nth-child(1) {
animation-delay: 0s;
}
#ticketsContainer .loading .spinner-grow:nth-child(2) {
animation-delay: .9s;
}
#ticketsContainer .loading .spinner-grow:nth-child(3) {
animation-delay: 1.8s;
}
.bg-none {
background: none;
}
.border-none {
border: none
}
a {
text-decoration: none;
}
body {
font-family: Roboto, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
color: #72777a;
line-height: 1.5;
letter-spacing: 0.2px;
overflow-x: hidden;
}
.bdT {
border-top: 1px solid var(--border-colour) !important;
}
.bdB {
border-bottom: 1px solid var(--border-colour) !important;
}
.w-fc {
width: fit-content;
}
.h-fc {
height: fit-content;
}
/* Ticket List Items */
.ticket-item {
cursor: pointer;
}
.ticket-item.active {
background-color: var(--bs-tertiary-bg);
}
.ticket-item:hover {
background-color: var(--bs-tertiary-bg);
}
/*
.ticket-item .ticket-item-indicator {
writing-mode: vertical-rl;
text-orientation: upright;
} */
.ticket-item .ticket-item-complex {
transition: all 0.3s ease;
overflow: hidden;
width: 2rem;
}
#ticketsContainer:not(.complex-items) .ticket-item .ticket-item-complex {
margin: 0 !important;
width: 0 !important;
}
.ticket-item .ticket-item-complex .badge {
min-width: 1.8rem;
}
.ticket-item .ticket-item-icon {
width: 2rem;
height: 2rem;
border-radius: 50%;
object-fit: cover;
}
.ticket-item .ticket-item-author {
margin-bottom: 0;
}
.ticket-item .ticket-item-title {
color: var(--bs-body-color);
font-size: 1rem;
text-transform: capitalize;
}
.ticket-item .ticket-item-desc {
color: var(--bs-secondary-color);
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-height: 3rem;
}
.ticket-item .ticket-item-tags .ticket-content-badge:last-child {
margin-right: 0 !important;
}
.loading .email-list-item {
cursor: progress;
}
/* Ticket Content */
.ticket-content .ticket-content-title {
color: var(--bs-body-color);
}
.ticket-content .ticket-content-desc {
color: var(--bs-secondary-color);
}
.ticket-content .ticket-content-icon {
border-radius: 50%;
width: 3rem;
height: 3rem;
object-fit: cover;
}
.ticket-content .ticket-content.author {
/* color: var(--bs-tertiary-color); */
margin-bottom: 5px;
}
/* Tickets Side Nav */
.email-app .email-side-nav {
border-right: 1px solid var(--border-colour);
}
.email-app .email-side-nav .nav-item .nav-link {
color: var(--bs-tertiary-color);
}
.email-app .email-wrapper {
min-height: 0;
}
@media screen and (min-width: 992px) {
.email-app .email-wrapper .email-list {
border-right: 1px solid var(--border-colour);
}
}
/* Base Sidebar Navigation */
.sidebar-menu {
border-right: 1px solid var(--border-colour);
}
.sidebar-logo {
border-right: 1px solid var(--border-colour);
border-bottom: 1px solid var(--border-colour);
}
/* Top Navigation */
.header {
border-bottom: 1px solid var(--border-colour);
}

View File

@ -0,0 +1,136 @@
svg {
touch-action: none;
}
.jvectormap-container {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
touch-action: none;
}
.jvectormap-tip {
position: absolute;
display: none;
border: solid 1px #CDCDCD;
border-radius: 3px;
background: #292929;
color: white;
font-family: sans-serif, Verdana;
font-size: smaller;
padding: 3px;
}
.jvectormap-zoomin, .jvectormap-zoomout, .jvectormap-goback {
position: absolute;
left: 10px;
border-radius: 3px;
background: #292929;
padding: 3px;
color: white;
cursor: pointer;
line-height: 10px;
text-align: center;
box-sizing: content-box;
}
.jvectormap-zoomin, .jvectormap-zoomout {
width: 10px;
height: 10px;
}
.jvectormap-zoomin {
top: 10px;
}
.jvectormap-zoomout {
top: 30px;
}
.jvectormap-goback {
bottom: 10px;
z-index: 1000;
padding: 6px;
}
.jvectormap-spinner {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: center no-repeat url(data:image/gif;base64,R0lGODlhIAAgAPMAAP///wAAAMbGxoSEhLa2tpqamjY2NlZWVtjY2OTk5Ly8vB4eHgQEBAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAIAAgAAAE5xDISWlhperN52JLhSSdRgwVo1ICQZRUsiwHpTJT4iowNS8vyW2icCF6k8HMMBkCEDskxTBDAZwuAkkqIfxIQyhBQBFvAQSDITM5VDW6XNE4KagNh6Bgwe60smQUB3d4Rz1ZBApnFASDd0hihh12BkE9kjAJVlycXIg7CQIFA6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YJvpJivxNaGmLHT0VnOgSYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ/V/nmOM82XiHRLYKhKP1oZmADdEAAAh+QQJCgAAACwAAAAAIAAgAAAE6hDISWlZpOrNp1lGNRSdRpDUolIGw5RUYhhHukqFu8DsrEyqnWThGvAmhVlteBvojpTDDBUEIFwMFBRAmBkSgOrBFZogCASwBDEY/CZSg7GSE0gSCjQBMVG023xWBhklAnoEdhQEfyNqMIcKjhRsjEdnezB+A4k8gTwJhFuiW4dokXiloUepBAp5qaKpp6+Ho7aWW54wl7obvEe0kRuoplCGepwSx2jJvqHEmGt6whJpGpfJCHmOoNHKaHx61WiSR92E4lbFoq+B6QDtuetcaBPnW6+O7wDHpIiK9SaVK5GgV543tzjgGcghAgAh+QQJCgAAACwAAAAAIAAgAAAE7hDISSkxpOrN5zFHNWRdhSiVoVLHspRUMoyUakyEe8PTPCATW9A14E0UvuAKMNAZKYUZCiBMuBakSQKG8G2FzUWox2AUtAQFcBKlVQoLgQReZhQlCIJesQXI5B0CBnUMOxMCenoCfTCEWBsJColTMANldx15BGs8B5wlCZ9Po6OJkwmRpnqkqnuSrayqfKmqpLajoiW5HJq7FL1Gr2mMMcKUMIiJgIemy7xZtJsTmsM4xHiKv5KMCXqfyUCJEonXPN2rAOIAmsfB3uPoAK++G+w48edZPK+M6hLJpQg484enXIdQFSS1u6UhksENEQAAIfkECQoAAAAsAAAAACAAIAAABOcQyEmpGKLqzWcZRVUQnZYg1aBSh2GUVEIQ2aQOE+G+cD4ntpWkZQj1JIiZIogDFFyHI0UxQwFugMSOFIPJftfVAEoZLBbcLEFhlQiqGp1Vd140AUklUN3eCA51C1EWMzMCezCBBmkxVIVHBWd3HHl9JQOIJSdSnJ0TDKChCwUJjoWMPaGqDKannasMo6WnM562R5YluZRwur0wpgqZE7NKUm+FNRPIhjBJxKZteWuIBMN4zRMIVIhffcgojwCF117i4nlLnY5ztRLsnOk+aV+oJY7V7m76PdkS4trKcdg0Zc0tTcKkRAAAIfkECQoAAAAsAAAAACAAIAAABO4QyEkpKqjqzScpRaVkXZWQEximw1BSCUEIlDohrft6cpKCk5xid5MNJTaAIkekKGQkWyKHkvhKsR7ARmitkAYDYRIbUQRQjWBwJRzChi9CRlBcY1UN4g0/VNB0AlcvcAYHRyZPdEQFYV8ccwR5HWxEJ02YmRMLnJ1xCYp0Y5idpQuhopmmC2KgojKasUQDk5BNAwwMOh2RtRq5uQuPZKGIJQIGwAwGf6I0JXMpC8C7kXWDBINFMxS4DKMAWVWAGYsAdNqW5uaRxkSKJOZKaU3tPOBZ4DuK2LATgJhkPJMgTwKCdFjyPHEnKxFCDhEAACH5BAkKAAAALAAAAAAgACAAAATzEMhJaVKp6s2nIkolIJ2WkBShpkVRWqqQrhLSEu9MZJKK9y1ZrqYK9WiClmvoUaF8gIQSNeF1Er4MNFn4SRSDARWroAIETg1iVwuHjYB1kYc1mwruwXKC9gmsJXliGxc+XiUCby9ydh1sOSdMkpMTBpaXBzsfhoc5l58Gm5yToAaZhaOUqjkDgCWNHAULCwOLaTmzswadEqggQwgHuQsHIoZCHQMMQgQGubVEcxOPFAcMDAYUA85eWARmfSRQCdcMe0zeP1AAygwLlJtPNAAL19DARdPzBOWSm1brJBi45soRAWQAAkrQIykShQ9wVhHCwCQCACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiRMDjI0Fd30/iI2UA5GSS5UDj2l6NoqgOgN4gksEBgYFf0FDqKgHnyZ9OX8HrgYHdHpcHQULXAS2qKpENRg7eAMLC7kTBaixUYFkKAzWAAnLC7FLVxLWDBLKCwaKTULgEwbLA4hJtOkSBNqITT3xEgfLpBtzE/jiuL04RGEBgwWhShRgQExHBAAh+QQJCgAAACwAAAAAIAAgAAAE7xDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfZiCqGk5dTESJeaOAlClzsJsqwiJwiqnFrb2nS9kmIcgEsjQydLiIlHehhpejaIjzh9eomSjZR+ipslWIRLAgMDOR2DOqKogTB9pCUJBagDBXR6XB0EBkIIsaRsGGMMAxoDBgYHTKJiUYEGDAzHC9EACcUGkIgFzgwZ0QsSBcXHiQvOwgDdEwfFs0sDzt4S6BK4xYjkDOzn0unFeBzOBijIm1Dgmg5YFQwsCMjp1oJ8LyIAACH5BAkKAAAALAAAAAAgACAAAATwEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GGl6NoiPOH16iZKNlH6KmyWFOggHhEEvAwwMA0N9GBsEC6amhnVcEwavDAazGwIDaH1ipaYLBUTCGgQDA8NdHz0FpqgTBwsLqAbWAAnIA4FWKdMLGdYGEgraigbT0OITBcg5QwPT4xLrROZL6AuQAPUS7bxLpoWidY0JtxLHKhwwMJBTHgPKdEQAACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GAULDJCRiXo1CpGXDJOUjY+Yip9DhToJA4RBLwMLCwVDfRgbBAaqqoZ1XBMHswsHtxtFaH1iqaoGNgAIxRpbFAgfPQSqpbgGBqUD1wBXeCYp1AYZ19JJOYgH1KwA4UBvQwXUBxPqVD9L3sbp2BNk2xvvFPJd+MFCN6HAAIKgNggY0KtEBAAh+QQJCgAAACwAAAAAIAAgAAAE6BDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfYIDMaAFdTESJeaEDAIMxYFqrOUaNW4E4ObYcCXaiBVEgULe0NJaxxtYksjh2NLkZISgDgJhHthkpU4mW6blRiYmZOlh4JWkDqILwUGBnE6TYEbCgevr0N1gH4At7gHiRpFaLNrrq8HNgAJA70AWxQIH1+vsYMDAzZQPC9VCNkDWUhGkuE5PxJNwiUK4UfLzOlD4WvzAHaoG9nxPi5d+jYUqfAhhykOFwJWiAAAIfkECQoAAAAsAAAAACAAIAAABPAQyElpUqnqzaciSoVkXVUMFaFSwlpOCcMYlErAavhOMnNLNo8KsZsMZItJEIDIFSkLGQoQTNhIsFehRww2CQLKF0tYGKYSg+ygsZIuNqJksKgbfgIGepNo2cIUB3V1B3IvNiBYNQaDSTtfhhx0CwVPI0UJe0+bm4g5VgcGoqOcnjmjqDSdnhgEoamcsZuXO1aWQy8KAwOAuTYYGwi7w5h+Kr0SJ8MFihpNbx+4Erq7BYBuzsdiH1jCAzoSfl0rVirNbRXlBBlLX+BP0XJLAPGzTkAuAOqb0WT5AH7OcdCm5B8TgRwSRKIHQtaLCwg1RAAAOwAAAAAAAAAAAA==);
}
.jvectormap-legend-title {
font-weight: bold;
font-size: 14px;
text-align: center;
}
.jvectormap-legend-cnt {
position: absolute;
}
.jvectormap-legend-cnt-h {
bottom: 0;
right: 0;
}
.jvectormap-legend-cnt-v {
top: 0;
right: 0;
}
.jvectormap-legend {
background: black;
color: white;
border-radius: 3px;
}
.jvectormap-legend-cnt-h .jvectormap-legend {
float: left;
margin: 0 10px 10px 0;
padding: 3px 3px 1px 3px;
}
.jvectormap-legend-cnt-h .jvectormap-legend .jvectormap-legend-tick {
float: left;
}
.jvectormap-legend-cnt-v .jvectormap-legend {
margin: 10px 10px 0 0;
padding: 3px;
}
.jvectormap-legend-cnt-h .jvectormap-legend-tick {
width: 40px;
}
.jvectormap-legend-cnt-h .jvectormap-legend-tick-sample {
height: 15px;
}
.jvectormap-legend-cnt-v .jvectormap-legend-tick-sample {
height: 20px;
width: 20px;
display: inline-block;
vertical-align: middle;
}
.jvectormap-legend-tick-text {
font-size: 12px;
}
.jvectormap-legend-cnt-h .jvectormap-legend-tick-text {
text-align: center;
}
.jvectormap-legend-cnt-v .jvectormap-legend-tick-text {
display: inline-block;
vertical-align: middle;
line-height: 20px;
padding-left: 3px;
}

View File

@ -0,0 +1,116 @@
/*
* Container style
*/
.ps {
overflow: hidden !important;
overflow-anchor: none;
-ms-overflow-style: none;
touch-action: auto;
-ms-touch-action: auto;
}
/*
* Scrollbar rail styles
*/
.ps__rail-x {
display: none;
opacity: 0;
transition: background-color .2s linear, opacity .2s linear;
-webkit-transition: background-color .2s linear, opacity .2s linear;
height: 15px;
/* there must be 'bottom' or 'top' for ps__rail-x */
bottom: 0px;
/* please don't change 'position' */
position: absolute;
}
.ps__rail-y {
display: none;
opacity: 0;
transition: background-color .2s linear, opacity .2s linear;
-webkit-transition: background-color .2s linear, opacity .2s linear;
width: 15px;
/* there must be 'right' or 'left' for ps__rail-y */
right: 0;
/* please don't change 'position' */
position: absolute;
}
.ps--active-x > .ps__rail-x,
.ps--active-y > .ps__rail-y {
display: block;
background-color: transparent;
}
.ps:hover > .ps__rail-x,
.ps:hover > .ps__rail-y,
.ps--focus > .ps__rail-x,
.ps--focus > .ps__rail-y,
.ps--scrolling-x > .ps__rail-x,
.ps--scrolling-y > .ps__rail-y {
opacity: 0.6;
}
.ps .ps__rail-x:hover,
.ps .ps__rail-y:hover,
.ps .ps__rail-x:focus,
.ps .ps__rail-y:focus,
.ps .ps__rail-x.ps--clicking,
.ps .ps__rail-y.ps--clicking {
background-color: #eee;
opacity: 0.9;
}
/*
* Scrollbar thumb styles
*/
.ps__thumb-x {
background-color: #aaa;
border-radius: 6px;
transition: background-color .2s linear, height .2s ease-in-out;
-webkit-transition: background-color .2s linear, height .2s ease-in-out;
height: 6px;
/* there must be 'bottom' for ps__thumb-x */
bottom: 2px;
/* please don't change 'position' */
position: absolute;
}
.ps__thumb-y {
background-color: #aaa;
border-radius: 6px;
transition: background-color .2s linear, width .2s ease-in-out;
-webkit-transition: background-color .2s linear, width .2s ease-in-out;
width: 6px;
/* there must be 'right' for ps__thumb-y */
right: 2px;
/* please don't change 'position' */
position: absolute;
}
.ps__rail-x:hover > .ps__thumb-x,
.ps__rail-x:focus > .ps__thumb-x,
.ps__rail-x.ps--clicking .ps__thumb-x {
background-color: #999;
height: 11px;
}
.ps__rail-y:hover > .ps__thumb-y,
.ps__rail-y:focus > .ps__thumb-y,
.ps__rail-y.ps--clicking .ps__thumb-y {
background-color: #999;
width: 11px;
}
/* MS supports */
@supports (-ms-overflow-style: none) {
.ps {
overflow: auto !important;
}
}
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
.ps {
overflow: auto !important;
}
}

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 434 KiB

View File

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 229 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

Before

Width:  |  Height:  |  Size: 492 KiB

After

Width:  |  Height:  |  Size: 492 KiB

View File

Before

Width:  |  Height:  |  Size: 160 B

After

Width:  |  Height:  |  Size: 160 B

View File

Before

Width:  |  Height:  |  Size: 148 B

After

Width:  |  Height:  |  Size: 148 B

View File

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 201 B

View File

Before

Width:  |  Height:  |  Size: 158 B

After

Width:  |  Height:  |  Size: 158 B

View File

Before

Width:  |  Height:  |  Size: 146 B

After

Width:  |  Height:  |  Size: 146 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

373
apps/static/js/_tickets.js Normal file
View File

@ -0,0 +1,373 @@
var displayedTicketID = -1;
filters = {"ordering": "-edit_timestamp", "page_size": 100};
editor = null;
searchTimeout = null;
loadingTickets = false;
const formControls = [
{
id: "newTitle",
validation: function(element) {
const value = element.val();
return (!element.attr("required") || value.trim() !== "")
},
errorMessage: function(element) {
return "This field is required."
}
}
];
$(document).ready(function() {
ClassicEditor
.create( document.getElementById("newDesc"), {})
.then( newEditor => {
editor = newEditor;
})
.catch( error => {
console.error(error)
});
$("#searchTickets").keyup(() => {
$("#ticketsContainer .content").empty();
$("#ticketsContainer .none-found").hide();
$("#ticketsContainer .loading").show();
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
console.debug("searching");
value = $("#searchTickets").val();
if (value === "") {
console.debug("deleted search filters");
delete filters["search"];
}
else {
console.debug("updated search filters");
filters["search"] = value;
}
loadAllTickets();
}, 500);
})
setupFilter("#filterSidebar .filter-department", "author__department");
setupFilter("#filterSidebar .filter-tags", "tags");
setupFilter("#filterSidebar .filter-priority", "priority");
loadFilterCounts();
loadAllTickets();
$(".select2-selection__choice").each(function() {
// Work out how to apply the colours of the tags to the selection choices.
})
});
function setupFilter(selector, key) {
$(selector).each(function () {
var input = $(this).find("input[type=checkbox], input[type=radio]");
var uuid = input.val();
input.on("change", function () {
if (input.is(":checkbox")) {
if ($(this).is(":checked")) {
filters[key] = filters[key] || [];
filters[key].push(uuid);
}
else {
filters[key] = filters[key].filter(id => id !== uuid);
if (filters[key].length === 0) {
delete filters[key];
}
}
}
else if (input.is(":radio") && input.is(":checked")) {
if (uuid === "all") {
delete filters[key];
}
else {
filters[key] = [uuid];
}
}
console.debug(`Filter applied '${key}' as '${uuid}'`)
loadAllTickets();
});
});
}
function validateForm() {
$("#ticketModal form").find(".form-control,.form-select").removeClass("is-valid is-invalid");
$("#ticketModal form .invalid-feedback").text("");
var valid = true;
formControls.forEach(function(control) {
var element = $("#" + control.id);
if (!control.validation(element)) {
element.addClass("is-invalid");
element.siblings(".invalid-feedback").text(control.errorMessage(element));
valid = false;
}
else {
element.addClass("is-valid");
}
});
return valid;
}
$("#ticketModal form").on("submit", function(event) {
event.preventDefault();
if (!validateForm()) {
return;
}
$.ajax({
url: URL_NewTicket,
type: "POST",
dataType: "json",
data: {
csrfmiddlewaretoken: CSRFMiddlewareToken,
title: $("#newTitle").val(),
description: editor.getData(),
author_id: CurrentUserID,
priority_id: $("#newPriority").val(),
tag_ids: $("#newTags").val()
},
success: function(data) {
loadAllTickets();
loadFilterCounts();
},
error: function(data) {
alert(JSON.stringify(data, null, 4))
}
});
});
function getOrdinalSuffix(day) {
if (day >= 11 && day <= 13) {
return day + 'th';
} else {
switch (day % 10) {
case 1: return day + 'st';
case 2: return day + 'nd';
case 3: return day + 'rd';
default: return day + 'th';
}
}
}
function updateFilterCounts(filterType, data) {
$("#filterSidebar .filter-" + filterType).each(function() {
var uuid = $(this).find("input[type=checkbox],input[type=radio]").val();
var count = data[filterType][uuid];
$(this).find(".badge").text(count);
});
}
function loadFilterCounts() {
$.ajax({
url: URL_FilterCounts,
type: "GET",
success: function(data) {
updateFilterCounts('priority', data);
updateFilterCounts('tags', data);
updateFilterCounts('department', data);
$("#filterPriorityAll .badge").text(data.tickets);
$("#filterDepartmentAll .badge").text(data.tickets)
$("#ticketCounts .total").text(data.tickets)
},
error: function(data) {
console.error(JSON.stringify(data, null, 4))
}
});
}
function timestampToHumanDate(timestamp, wasYesterday) {
if (wasYesterday) {
var day = getOrdinalSuffix(timestamp.getDate());
var month = timestamp.toLocaleString('en-GB', { month: 'short' });
var year = timestamp.toLocaleString('en-GB', { year: 'numeric' });
var time = timestamp.toLocaleString('en-GB', { hour: 'numeric', minute: 'numeric' });
return time + ', ' + day + ' ' + month + ' ' + year;
}
var hours = timestamp.getUTCHours();
var minutes = timestamp.getUTCMinutes();
return hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0');
}
function loadAllTickets() {
if (loadingTickets === true) {
return;
}
$("#ticketsContainer .content").empty();
$("#ticketsContainer .none-found").hide();
$("#ticketsContainer .loading").show();
loadingTickets = true;
// alert(JSON.stringify(filters, null, 4));
$.ajax({
url: URL_Tickets,
type: "GET",
dataType: "json",
data: $.param(filters, true),
success: function(data) {
loadingTickets = false;
// console.log(JSON.stringify(data, null, 4))
$("#ticketCounts .current").text(data.results.length);
$("#ticketsContainer .loading").hide();
if (data.results.length === 0) $("#ticketsContainer .none-found").show();
else $("#ticketsContainer .none-found").hide();
data.results.forEach(function(ticket) {
var timestamp = new Date(ticket.timestamp);
var formattedTime = timestampToHumanDate(timestamp, ticket.was_yesterday);
if (ticket.is_edited) {
formattedTime += " • edited";
}
var template = $($("#ticketItemTemplate").html());
template.find(".ticket-item-author").text(`${ticket.author.forename} ${ticket.author.surname}`);
template.find(".ticket-item-datetime").text(formattedTime);
template.find(".ticket-item-title").text(ticket.title);
template.find(".ticket-item-desc").text(ticket.description);
template.find(".ticket-item-icon").attr("src", ticket.author.icon);
template.attr("data-uuid", ticket.uuid);
$("#ticketsContainer .content").append(template);
});
applyTicketClickFunction();
},
error: function(data) {
loadingTickets = false;
$("#ticketsContainer .content").empty();
$("#ticketsContainer .none-found").hide();
$("#ticketsContainer .loading").hide();
if (data.status === 429) {
alert(`HTTP ${data.status} - ${data.statusText}\n${data.responseJSON.detail}`)
}
}
});
}
function applyTicketClickFunction() {
$(".ticket-item").on("click", function(e) {
e.preventDefault();
displayTicket(this);
$('.email-app').removeClass('side-active');
$('.email-content').toggleClass('open');
});
}
function reloadCurrentTicket() {
displayTicket($(".ticket-item.bgc-grey-100"));
}
function changeTicket(next=true) {
var selectedTicket = $(".ticket-item.bgc-grey-100");
if (!selectedTicket.length) {
displayTicket($(".ticket-item").first());
return;
}
if (next) {
displayTicket(selectedTicket.next());
}
else {
displayTicket(selectedTicket.prev());
}
if (!$('.email-content').hasClass('open')) {
$('.email-content').addClass('open');
}
}
function displayTicket(ticketElement) {
ticket = $(ticketElement);
ticketID = ticket.data("uuid");
// $(".back-to-mailbox").off("click").on("click", function(event) {
// event.preventDefault();
// $('.email-content').toggleClass('open');
// displayTicket(ticketElement);
// });
if (displayedTicketID === ticketID) {
// displayedTicketID = -1;
return;
}
ticket.siblings().removeClass("bgc-grey-100");
ticket.addClass("bgc-grey-100");
$("#ticketTitle").text("")
$("#ticketDesc").empty();
$("#ticketAuthor").text("");
$("#ticketAuthorImg").hide();
$("#ticketAuthorImg").prop("src", "");
$("#ticketTimestamp").text("");
$("#btnGroupDrop2").hide();
$("#ticketBadges").empty().hide();
displayedTicketID = ticketID;
$.ajax({
url: URL_Tickets,
type: 'get',
dataType: 'json',
data: $.param({uuid: ticketID}, true),
success: function (data) {
console.log(JSON.stringify(data, null, 4));
var ticket = data.results[0];
var author = ticket.author;
var department = author.department;
var priority = ticket.priority;
$("#ticketTitle").text(ticket.title);
$("#ticketDesc").append($(`<div class="w-100">${ticket.description}</div>`));
$("#ticketAuthor").text(`${author.forename} ${author.surname}`);
$("#ticketAuthorImg").show();
$("#ticketAuthorImg").prop("src", author.icon);
$("#btnGroupDrop2").show();
$("#ticketBadges").show();
$("#ticketBadges").append($(`<div class="badge me-1" style="color: ${priority.colour}; background-color: ${priority.backgroundcolour};">${priority.title} Priority <i class="ti-control-record "></i></div>`));
if (department != null) {
$("#ticketBadges").append($(`<div class="badge bgc-deep-purple-500 me-1">${department.title}</div>`));
}
ticket.tags.forEach(function(tag) {
$("#ticketBadges").append($(`<div class="badge me-1" style="color: ${tag.colour}; background-color: ${tag.backgroundcolour};">${tag.title} <i class="ti-tag"></i></div>`));
});
// timestamp
var timestamp = new Date(ticket.timestamp);
var formattedTime = timestampToHumanDate(timestamp, ticket.was_yesterday);
if (ticket.is_edited) {
formattedTime += " • edited";
}
$("#ticketTimestamp").text(formattedTime);
},
error: function(message) {
alert(JSON.stringify(message, null, 4));
}
});
}

23
apps/static/js/base.js Normal file
View File

@ -0,0 +1,23 @@
$(document).ready(function() {
// Activate all tooltips
$('[data-bs-toggle="tooltip"]').tooltip();
// Apply the user preferred theme
const theme = localStorage.getItem("theme");
if (theme == "light" || theme == "dark") {
$("body").attr("data-bs-theme", theme);
}
else {
$("body").attr("data-bs-theme", "light");
}
});
$("#themeToggle").on("click", function() {
var theme = $("body").attr("data-bs-theme");
theme = theme == "light" ? "dark" : "light";
localStorage.setItem("theme", theme)
$("body").attr("data-bs-theme", theme);
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

456
apps/static/js/tickets.js Normal file
View File

@ -0,0 +1,456 @@
var filters = {"ordering": "-edit_timestamp", "strict-tags": true};
global_loadingTickets = false;
searchTimeout = null;
pagination = {};
complexItems = false;
$(document).ready(function() {
initSearchBar();
toggleComplexItems(localStorage.getItem("hideComplexTickets") === "true");
setupFilter("#filterSidebar .filter-department", "author__department");
setupFilter("#filterSidebar .filter-tags", "tags");
setupFilter("#filterSidebar .filter-priority", "priority");
loadFilterCounts();
loadTicketItems();
});
function updateItemsState(state) {
console.debug(`updating items state to '${state}'`);
$("#ticketsContainer").scrollTop(0);
$("#ticketsContainer").trigger("updateScrollbar");
switch (state) {
case "content":
$("#ticketsContainer .none-found").hide();
$("#ticketsContainer .loading").hide();
$("#filterSidebar input").prop("disabled", false);
break;
case "loading":
$("#ticketsContainer .content").empty();
$("#ticketsContainer .none-found").hide();
$("#ticketsContainer .loading").show();
$("#filterSidebar input").prop("disabled", true);
break;
case "no-content":
$("#ticketsContainer .content").empty();
$("#ticketsContainer .none-found").show();
$("#ticketsContainer .loading").hide();
$("#filterSidebar input").prop("disabled", false);
break;
default:
throw new Error(`Invalid Items State '${state}'`);
}
}
function updateContentState(state) {
console.debug(`updating content state to '${state}'`);
switch (state) {
case "content":
$("#ticketContent .loading").hide();
break;
case "loading":
$("#ticketContent .content").empty();
$("#ticketContent .loading").show();
break;
default:
throw new Error(`Invalid Content State '${state}'`);
}
}
function initSearchBar() {
$("#searchTickets").keyup(() => {
updateItemsState("loading");
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
console.debug("searching");
value = $("#searchTickets").val();
if (value === "") {
console.debug("deleted search filters");
delete filters["search"];
}
else {
console.debug("updated search filters");
filters["search"] = value;
}
loadTicketItems();
}, 500);
})
}
function setupFilter(selector, key) {
$(selector).each(function () {
var input = $(this).find("input[type=checkbox], input[type=radio]");
var uuid = input.val();
input.on("change", function () {
if (input.is(":checkbox")) {
if ($(this).is(":checked")) {
filters[key] = filters[key] || [];
filters[key].push(uuid);
}
else {
filters[key] = filters[key].filter(id => id !== uuid);
if (filters[key].length === 0) delete filters[key];
}
}
else if (input.is(":radio") && input.is(":checked")) {
if (uuid === "all") delete filters[key];
else filters[key] = [uuid];
const deselectOption = $(`[name='${input.prop("name")}'].deselect-radio-filters`);
if (deselectOption.length) {
if (deselectOption.prop("checked")) deselectOption.parent().hide();
else deselectOption.parent().show();
}
}
console.debug(`Filter applied '${key}' as '${uuid}'`)
loadTicketItems();
});
});
}
function getOrdinalSuffix(day) {
if (day >= 11 && day <= 13) {
return day + 'th';
} else {
switch (day % 10) {
case 1: return day + 'st';
case 2: return day + 'nd';
case 3: return day + 'rd';
default: return day + 'th';
}
}
}
function timestampToHumanDate(timestamp, wasYesterday) {
if (wasYesterday) {
var day = getOrdinalSuffix(timestamp.getDate());
var month = timestamp.toLocaleString('en-GB', { month: 'short' });
var year = timestamp.toLocaleString('en-GB', { year: 'numeric' });
var time = timestamp.toLocaleString('en-GB', { hour: 'numeric', minute: 'numeric' });
return time + ', ' + day + ' ' + month + ' ' + year;
}
var hours = timestamp.getUTCHours();
var minutes = timestamp.getUTCMinutes();
return hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0');
}
function updateFilterCounts(filterType, data) {
$("#filterSidebar .filter-" + filterType).each(function() {
var uuid = $(this).find("input[type=checkbox],input[type=radio]").val();
var count = data[filterType][uuid];
$(this).find(".badge").text(count);
});
}
function loadFilterCounts() {
$.ajax({
url: URL_FilterCounts,
type: "GET",
success: function(data) {
updateFilterCounts('priority', data);
updateFilterCounts('tags', data);
updateFilterCounts('department', data);
// $("#filterPriorityAll .badge").text(data.tickets);
// $("#filterDepartmentAll .badge").text(data.tickets);
// $("#ticketCounts .total").text(data.tickets);
},
error: function(data) {
console.error(JSON.stringify(data, null, 4))
}
});
}
function applyTicketClickFunction() {
$(".ticket-item").on("click", function(e) {
e.preventDefault();
if ($(this).hasClass("active")) {
return;
}
loadTicketContent($(this).data("uuid"));
$(".ticket-item").removeClass("active");
$(this).addClass("active");
$('.email-app').removeClass('side-active');
$('.email-content').toggleClass('open');
});
}
$(".back-to-mailbox").on("click", function(e) {
e.preventDefault();
$(".ticket-item.active").removeClass("active");
});
function reloadCurrentTicket() {
loadTicketContent($(".ticket-item.active").data("uuid"));
}
function changeTicket(next=true) {
var selectedTicket = $(".ticket-item.active");
var uuid;
if (!selectedTicket.length) selectedTicket = $(".ticket-item").first();
else if (next) selectedTicket = selectedTicket.next();
else selectedTicket = selectedTicket.prev();
$(".ticket-item").removeClass("active");
selectedTicket.addClass("active");
uuid = selectedTicket.data("uuid");
loadTicketContent(uuid);
if (!$('.email-content').hasClass('open')) {
$('.email-content').addClass('open');
}
}
function changeItemsPage(next) {
$("#ticketItemsNextPage").prop("disabled", true);
$("#ticketItemsPrevPage").prop("disabled", true);
var page = pagination.page;
if (next && pagination.next) page ++;
else if (!next && pagination.prev) page --;
else return;
loadTicketItems(page);
}
$("#strictTags").on("change", function() {
const strictTags = $(this).prop("checked");
if (strictTags) filters["strict-tags"] = strictTags;
else delete filters["strict-tags"];
loadTicketItems();
});
/**
* Loads the list of ticket items using the current filters.
*
* @function loadTicketItems
* @param {Number} page For pagination, an invalid page will result in an error.
*/
function loadTicketItems(page=1) {
if (global_loadingTickets) {
alert("Spam prevention\nStopped loadTicketItems because already loading.");
return;
}
global_loadingTickets = true;
updateItemsState("loading");
filters["page"] = page;
var fetchFilters = { ...filters };
fetchFilters["only_fields"] = "uuid,title,short_description,author,priority,tags,timestamp,is_edited,author__forename,author__surname,author__department,author__icon,display_datetime";
fetchTicketsPromise(fetchFilters).then((response) => {
// Update the counts to show how many tickets were found
$("#ticketCounts .current").text(response.results.length);
$("#ticketCounts .total").text(response.count);
// If there are no tickets
if (!response.count) {
updateItemsState("no-content");
return;
}
const ticketHasNextPage = typeof response.next === "string";
const ticketHasPrevPage = typeof response.previous === "string";
$("#ticketItemsNextPage").prop("disabled", !ticketHasNextPage);
$("#ticketItemsPrevPage").prop("disabled", !ticketHasPrevPage);
const pageNumber = filters["page"] || 1; // If we haven't tracked the page, it's safe to assume it's 1,
pagination = { // because we always track it when it changes.
"page": pageNumber,
"next": ticketHasNextPage,
"prev": ticketHasPrevPage
}
// Update the pagination count with the current page
$("#paginationCounts .current").text(pageNumber);
// If we are on page one (which has been normalised to be the case in many intances)
// We can use the amount of results to calculate the total amount of pages.
if (pageNumber === 1) {
$("#paginationCounts .total").text(Math.ceil(response.count / response.results.length));
}
// Iterate over and handle each ticket
response.results.forEach(function(ticket) {
$("#ticketsContainer .content").append(createTicketItem(ticket));
});
// Make tickets clickable
applyTicketClickFunction();
updateItemsState("content");
global_loadingTickets = false;
});
}
/**
* Returns a jquery object representing an element for a ticket item, constructed using the passed
* ticket and a predefined template.
*
* @function createTicketItem
* @param {Object} ticket An object representing a ticket.
* @return {jQuery} ticketElement to be appeneded as content.
*/
function createTicketItem(ticket) {
// Create a copy of the template using the ticket data
var template = $($("#ticketItemTemplate").html());
template.find(".ticket-item-author").text(`${ticket.author.forename} ${ticket.author.surname}`);
template.find(".ticket-item-datetime").text(ticket.display_datetime);
template.find(".ticket-item-title").text(ticket.title);
template.find(".ticket-item-desc").html(ticket.short_description);
template.find(".ticket-item-icon").attr("src", ticket.author.icon);
template.attr("data-uuid", ticket.uuid);
// Add tickets using the badge template
ticket.tags.forEach(function(tag) {
var tagTemplate = $($("#ticketContentBadgeTemplate").html());
tagTemplate.find(".ticket-content-badge-text").text(tag.title);
tagTemplate.css({ "color": tag.colour, "background-color": tag.backgroundcolour });
template.find(".ticket-item-tags").append(tagTemplate);
});
const priority = ticket.priority;
var priorityElem = template.find(".ticket-item-priority");
priorityElem.css("color", priority.colour);
priorityElem.css("background-color", priority.backgroundcolour);
priorityElem.attr("data-bs-title", priority.title + " Priority");
priorityElem.tooltip();
const department = ticket.author.department;
var departmentElem = template.find(".ticket-item-department");
if (department === null) {
departmentElem.hide();
}
else {
departmentElem.css("color", ticket.author.department.colour);
departmentElem.css("background-color", ticket.author.department.backgroundcolour);
departmentElem.attr("data-bs-title", ticket.author.department.title + " Department");
departmentElem.tooltip();
}
return template;
}
/**
* Load the content of a selected ticket.
*
* @function loadTicketContent
* @param {String} uuid A string representation of the ticket's UUID.
*/
function loadTicketContent(uuid) {
updateContentState("loading");
if (global_loadingTickets) {
console.debug("Spam prevention\nStopped loadTicketContent because already loading.");
return;
}
global_loadingTickets = true;
$("#ticketContent .content").empty();
fetchTicketsPromise({uuid: uuid}).then((response) => {
ticket = response.results[0];
$("#ticketContent .content").append(createTicketContent(ticket));
updateContentState("content");
global_loadingTickets = false;
});
}
/**
* Returns a jquery object representing an element for a ticket content, constructed using the
* passed ticket and a predefined template.
*
* @function createTicketItem
* @param {Object} ticket An object representing a ticket.
* @return {jQuery} ticketElement to be shown as content.
*/
function createTicketContent(ticket) {
// Create a copy of the template using the ticket data
var template = $($("#ticketContentTemplate").html());
template.find(".ticket-content-author").text(`${ticket.author.forename} ${ticket.author.surname}`);
template.find(".ticket-content-datetime").text(ticket.display_datetime);
template.find(".ticket-content-title").text(ticket.title);
template.find(".ticket-content-desc").html(ticket.description);
template.find(".ticket-content-icon").attr("src", ticket.author.icon);
console.debug(ticket.description);
ticket.tags.forEach(function(tag) {
var tagTemplate = $($("#ticketContentBadgeTemplate").html());
tagTemplate.find(".ticket-content-badge-text").text(tag.title);
tagTemplate.css({ "color": tag.colour, "background-color": tag.backgroundcolour });
template.find(".ticket-content-badges").append(tagTemplate);
});
var departmentElem = template.find(".ticket-content-department");
if (ticket.author.department === null) {
template.find(".ticket-content-department").hide();
}
else {
}
var priorityElem = template.find(".ticket-content-priority");
priorityElem.css({"color": ticket.priority.colour, "background-color": ticket.priority.backgroundcolour});
priorityElem.attr("data-bs-title", ticket.priority.title);
priorityElem.tooltip();
return template;
}
function fetchTicketsPromise(queryFilters) {
return new Promise(function(resolve, reject) {
global_loadingTickets = true;
$.ajax({
url: URL_Tickets,
type: "GET",
dataType: "JSON",
data: $.param(queryFilters, true),
success: function(response) {
global_loadingTickets = false;
resolve(response);
},
error: function(response) {
global_loadingTickets = false;
if (response.status === 429) {
alert(`
HTTP ${response.status} - ${response.statusText}\n
${response.responseJSON.detail}
`);
}
reject(response);
}
});
});
}
// Prevent certain dropdowns from closing when the user clicks.
$(".dropdown-menu.prevent-click-close").on("click", function(e) {
e.stopPropagation();
});
function toggleComplexItems(hideComplex=null) {
if (hideComplex === null) {
hideComplex = !(localStorage.getItem("hideComplexTickets") === "true");
}
if (hideComplex) $("#ticketsContainer").removeClass("complex-items");
else $("#ticketsContainer").addClass("complex-items");
localStorage.setItem("hideComplexTickets", hideComplex);
}

View File

@ -1,4 +1,5 @@
{% extends "layouts/base-authentication.html" %}
{% load static %}
{% block title %} Sign IN {% endblock title %}
@ -8,10 +9,10 @@
{% block content %}
<div class="peers ai-s fxw-nw h-100vh">
<div class="d-n@sm- peer peer-greed h-100 pos-r bgr-n bgpX-c bgpY-c bgsz-cv" style='background-image: url("{{ ASSETS_ROOT }}/images/bg.jpg")'>
<div class="d-n@sm- peer peer-greed h-100 pos-r bgr-n bgpX-c bgpY-c bgsz-cv" style='background-image: url("{% static '/images/bg.jpg' %}")'>
<div class="pos-a centerXY">
<div class="bgc-white bdrs-50p pos-r" style='width: 120px; height: 120px;'>
<img class="pos-a centerXY" src="{{ ASSETS_ROOT }}/images/logo.png" alt="">
<img class="pos-a centerXY" src="{% static '/images/logo.png' %}" alt="">
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
{% extends "layouts/base-authentication.html" %}
{% load static %}
{% block title %} Register {% endblock title %}
@ -8,10 +9,10 @@
{% block content %}
<div class="peers ai-s fxw-nw h-100vh">
<div class="peer peer-greed h-100 pos-r bgr-n bgpX-c bgpY-c bgsz-cv" style='background-image: url("{{ ASSETS_ROOT }}/images/bg.jpg")'>
<div class="peer peer-greed h-100 pos-r bgr-n bgpX-c bgpY-c bgsz-cv" style='background-image: url("{% static '/images/bg.jpg' %}")'>
<div class="pos-a centerXY">
<div class="bgc-white bdrs-50p pos-r" style='width: 120px; height: 120px;'>
<img class="pos-a centerXY" src="{{ ASSETS_ROOT }}/images/logo.png" alt="">
<img class="pos-a centerXY" src="{% static '/images/logo.png' %}" alt="">
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
{% extends "layouts/base-error.html" %}
{% load static %}
{% block title %} Error 404 {% endblock title %}
@ -9,7 +10,7 @@
<div class='pos-a t-0 l-0 bgc-white w-100 h-100 d-f fxd-r fxw-w ai-c jc-c pos-r p-30'>
<div class='mR-60'>
<img alt='#' src='{{ ASSETS_ROOT }}/images/404.png' />
<img alt='#' src="{% static '/images/404.png' %}" />
</div>
<div class='d-f jc-c fxd-c'>

View File

@ -1,4 +1,5 @@
{% extends "layouts/base-error.html" %}
{% load static %}
{% block title %} Error 500 {% endblock title %}
@ -9,7 +10,7 @@
<div class='pos-a t-0 l-0 bgc-white w-100 h-100 d-f fxd-r fxw-w ai-c jc-c pos-r p-30'>
<div class='mR-60'>
<img alt='#' src='{{ ASSETS_ROOT }}/images/500.png' />
<img alt='#' src="{% static '/images/500.png' %}" />
</div>
<div class='d-f jc-c fxd-c'>
@ -17,7 +18,7 @@
<h3 class='mB-10 fsz-lg c-grey-900 tt-c'>Internal server error</h3>
<p class='mB-30 fsz-def c-grey-700'>Something goes wrong with our servers, please try again later.</p>
<div>
<a href="index.html" type='primary' class='btn btn-primary'>Go to Home</a>
<a href="/" type='primary' class='btn btn-primary'>Go to Home</a>
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
{% extends "layouts/base-authentication.html" %}
{% load static %}
{% block title %} Sign IN {% endblock title %}
@ -8,10 +9,10 @@
{% block content %}
<div class="peers ai-s fxw-nw h-100vh">
<div class="d-n@sm- peer peer-greed h-100 pos-r bgr-n bgpX-c bgpY-c bgsz-cv" style='background-image: url("{{ ASSETS_ROOT }}/images/bg.jpg")'>
<div class="d-n@sm- peer peer-greed h-100 pos-r bgr-n bgpX-c bgpY-c bgsz-cv" style='background-image: url("{% static '/images/bg.jpg' %}")'>
<div class="pos-a centerXY">
<div class="bgc-white bdrs-50p pos-r" style='width: 120px; height: 120px;'>
<img class="pos-a centerXY" src="{{ ASSETS_ROOT }}/images/logo.png" alt="">
<img class="pos-a centerXY" src="{% static '/images/logo.png' %}" alt="">
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
{% extends "layouts/base-authentication.html" %}
{% load static %}
{% block title %} Register {% endblock title %}
@ -8,10 +9,10 @@
{% block content %}
<div class="peers ai-s fxw-nw h-100vh">
<div class="peer peer-greed h-100 pos-r bgr-n bgpX-c bgpY-c bgsz-cv" style='background-image: url("{{ ASSETS_ROOT }}/images/bg.jpg")'>
<div class="peer peer-greed h-100 pos-r bgr-n bgpX-c bgpY-c bgsz-cv" style='background-image: url("{% static '/images/bg.jpg' %}")'>
<div class="pos-a centerXY">
<div class="bgc-white bdrs-50p pos-r" style='width: 120px; height: 120px;'>
<img class="pos-a centerXY" src="{{ ASSETS_ROOT }}/images/logo.png" alt="">
<img class="pos-a centerXY" src="{% static '/images/logo.png' %}" alt="">
</div>
</div>
</div>

View File

@ -5,322 +5,340 @@
<!-- Specific CSS goes HERE -->
{% block stylesheets %}
<link rel="stylesheet" href="{{ ASSETS_ROOT }}/css/select2-bootstrap.min.css">
<link rel="stylesheet" href="{% static '/css/select2-bootstrap.min.css' %}">
{% endblock stylesheets %}
{% block content %}
<!-- ### $App Screen Content ### -->
<main class='main-content bgc-grey-100'>
<main class='main-content'>
<div id='mainContent'>
<div class="full-container">
<div class="full-container" style="overflow: hidden;">
<div class="email-app">
<div class="email-side-nav remain-height ov-h">
<div class="h-100 layers">
<div class="p-20 bgc-grey-100 layer w-100">
<div class="p-20 bg-body-tertiary layer w-100">
<button type="button" class="btn btn-danger c-white w-100" data-bs-toggle="modal" data-bs-target="#ticketModal">New Ticket</button>
</div>
<div class="scrollable pos-r bdT layer w-100 fxg-1">
<ul class="p-20 nav flex-column">
<li class="nav-item">
<h6>Filters</h6>
<div class="scrollable pos-r ov-h bdT layer w-100 fxg-1 bg-body">
<ul id="filterSidebar" class="p-20 nav flex-column">
{% if priorities %}
<li class="nav-item mT-10 filter-priority">
<h6 class="peers ai-c jc-sb mb-2 px-3">
<span class="peer-greed">Priorities</span>
<label for="filterPriority-all" class="nav-link text-reset actived p-0 cur-p" style="display: none">
<input type="radio" id="filterPriority-all" name="filterPriorities" class="btn-check deselect-radio-filters" checked="checked" value="all">
<span class="text-body badge small bg-none border-none py-0">
<i class="ti-close"></i>
</span>
</label>
</h6>
</li>
{% if priorities %}
<li class="nav-item px-3 mt-3">
<h6 class="small">Priority</h6>
{% for priority in priorities %}
<div class="form-check mb-2">
<input type="checkbox" id="priority-{{ priority.id }}" class="form-check-input me-3">
<label for="priority-{{ priority.id }}" class="form-check-label">
<span class="badge rounded-pill" style="color: {{ priority.colour }}; background-color: {{ priority.backgroundcolour }};">
{{ priority.title }}
<i class="ti-control-record ms-auto"></i>
</span>
</label>
</div>
{% endfor %}
</li>
{% endif %}
{% for priority in priorities %}
{% if departments %}
<li class="nav-item px-3 mt-3">
<h6 class="small">Department</h6>
{% for department in departments %}
<div class="form-check mb-2">
<input type="checkbox" id="department-{{ department.id }}" class="form-check-input me-2">
<label for="department-{{ department.id }}" class="form-check-label d-flex jc-sb">
{{ department.title }}
<i class="{{ department.icon }} ms-auto"></i>
</label>
<li class="nav-item filter-priority">
<label for="filterPriority-{{ priority.uuid }}" class="nav-link text-reset actived">
<div class="peers ai-c jc-sb">
<div class="peer peer-greed">
<input type="radio" id="filterPriority-{{ priority.uuid }}" name="filterPriorities" class="form-check-input me-2" value="{{ priority.uuid }}">
<span>{{ priority.title }}</span>
</div>
<div class="peer">
<span class="badge rounded-pill" style="color: {{ priority.colour }}; background-color: {{ priority.backgroundcolour }};">0</span>
</div>
</div>
{% endfor %}
</label>
</li>
{% endif %}
{% if tags %}
<li class="nav-item px-3 mt-3">
<h6 class="small">Tags</h6>
{% for tag in tags %}
<div class="form-check mb-2">
<input type="checkbox" id="tag-{{ tag.id }}" class="form-check-input me-3">
<label for="tag-{{ tag.id }}" class="form-check-label">
<span class="badge rounded-pill" style="color: {{ tag.colour }}; background-color: {{ tag.backgroundcolour }};">
{{ tag.title }}
<i class="ti-tag ms-auto"></i>
</span>
</label>
{% endfor %}
{% endif %}
<li class="nav-item">
<hr class="mY-30 border-secondary">
</li>
{% if tags %}
<li class="nav-item">
<h6 class="peers ai-c jc-sb mb-2 px-3">
<span class="peer-greed">Tags</span>
<div class="peer dropdown">
<span class="text-body badge small bg-none border-none py-0 cur-p" data-bs-toggle="dropdown">
<i class="ti-more-alt"></i>
</span>
<div class="dropdown-menu prevent-click-close mT-5">
<li class="px-3 py-2">
<label for="strictTags" class="form-check-label small mb-2 fw-normal">Only show tickets matching all selected tags?</label>
<div class="form-switch flex-wrap">
<input type="checkbox" name="strictTags" id="strictTags" class="form-check-input" checked="checked">
</div>
</li>
</div>
</div>
</h6>
</li>
{% for tag in tags %}
<li class="nav-item filter-tags">
<label for="filterTag-{{ tag.uuid }}" class="nav-link text-reset actived">
<div class="peers ai-c jc-sb">
<div class="peer peer-greed">
<input type="checkbox" id="filterTag-{{ tag.uuid }}" class="form-check-input me-2" value="{{ tag.uuid }}">
<span>{{ tag.title }}</span>
</div>
<div class="peer">
<span class="badge rounded-pill" style="color: {{ tag.colour }}; background-color: {{ tag.backgroundcolour }};">0</span>
</div>
</div>
{% endfor %}
</label>
</li>
{% endif %}
<!--
<li class="nav-item mt-5">
<a href="javascript:void(0)" class="nav-link c-grey-800 cH-blue-500 actived">
<div class="peers ai-c jc-sb">
<div class="peer peer-greed">
<i class="mR-10 ti-email"></i>
<span>Inbox</span>
</div>
<div class="peer">
<span class="badge rounded-pill bgc-deep-purple-50 c-deep-purple-700">+99</span>
</div>
</div>
</a>
</li> -->
<!--<li class="nav-item">
<a href="" class="nav-link c-grey-800 cH-blue-500">
<div class="peers ai-c jc-sb">
<div class="peer peer-greed">
<i class="mR-10 ti-share"></i>
<span>Sent</span>
</div>
<div class="peer">
<span class="badge rounded-pill bgc-green-50 c-green-700">12</span>
</div>
</div>
</a>
</li>
{% endfor %}
{% endif %}
<li class="nav-item">
<a href="" class="nav-link c-grey-800 cH-blue-500">
<div class="peers ai-c jc-sb">
<div class="peer peer-greed">
<i class="mR-10 ti-star"></i>
<span>Important</span>
</div>
<div class="peer">
<span class="badge rounded-pill bgc-blue-50 c-blue-700">3</span>
</div>
</div>
</a>
<hr class="mY-30 border-secondary">
</li>
<li class="nav-item">
<a href="" class="nav-link c-grey-800 cH-blue-500">
<div class="peers ai-c jc-sb">
<div class="peer peer-greed">
<i class="mR-10 ti-file"></i>
<span>Drafts</span>
</div>
<div class="peer">
<span class="badge rounded-pill bgc-amber-50 c-amber-700">5</span>
</div>
</div>
</a>
</li>
<li class="nav-item">
<a href="" class="nav-link c-grey-800 cH-blue-500">
<div class="peers ai-c jc-sb">
<div class="peer peer-greed">
<i class="mR-10 ti-alert"></i>
<span>Spam</span>
</div>
<div class="peer">
<span class="badge rounded-pill bgc-red-50 c-red-700">1</span>
</div>
</div>
</a>
</li>
<li class="nav-item">
<a href="" class="nav-link c-grey-800 cH-blue-500">
<div class="peers ai-c jc-sb">
<div class="peer peer-greed">
<i class="mR-10 ti-trash"></i>
<span>Trash</span>
</div>
<div class="peer">
<span class="badge rounded-pill bgc-red-50 c-red-700">+99</span>
</div>
</div>
</a>
</li> -->
{% if departments %}
<li id="filterDepartmentAll" class="nav-item filter-department">
<h6 class="peers ai-c jc-sb mb-2 px-3">
<span class="peer-greed">Departments</span>
<label for="filterDepartment-all" class="nav-link text-reset actived p-0 cur-p" style="display: none">
<input type="radio" id="filterDepartment-all" name="filterDepartment" class="btn-check deselect-radio-filters" checked="checked" value="all">
<span class="text-body badge small bg-none border-none py-0">
<i class="ti-close"></i>
</span>
</label>
</h6>
</li>
{% for department in departments %}
<li class="nav-item filter-department">
<label for="filterDepartment-{{ department.uuid }}" class="nav-link text-reset actived">
<div class="peers ai-c jc-sb">
<div class="peer peer-greed">
<input type="radio" id="filterDepartment-{{ department.uuid }}" name="filterDepartment" class="form-check-input me-2" value="{{ department.uuid }}">
<span>{{ department.title }}</span>
</div>
<div class="peer">
<span class="badge rounded-pill" style="color: {{ department.colour }}; background-color: {{ department.backgroundcolour }};">0</span>
</div>
</div>
</label>
</li>
{% endfor %}
{% endif %}
</ul>
</div>
</div>
</div>
<div class="email-wrapper row remain-height bgc-white ov-h">
<div class="email-wrapper row remain-height bg-body">
<div class="email-list h-100 layers">
<div class="layer w-100">
<div class="bgc-grey-100 peers ai-c p-20 fxw-nw">
<div class="peer me-auto">
<div class="btn-group" role="group">
<button type="button" class="email-side-toggle d-n@md+ btn bgc-white bdrs-2 mR-3 cur-p">
<i class="ti-menu"></i>
</button>
<button type="button" class="btn bgc-white bdrs-2 mR-3 cur-p">
<i class="ti-folder"></i>
</button>
<button type="button" class="btn bgc-white bdrs-2 mR-3 cur-p">
<i class="ti-tag"></i>
</button>
<div class="bg-body-tertiary peers ai-c p-20 fxw-nw">
<div class="peer me-2">
<div class="btn-group" role="group">
<button id="btnGroupDrop1" type="button" class="btn cur-p bgc-white no-after dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="ti-more-alt"></i>
<button type="button" class="email-side-toggle d-n@md+ btn bg-body bdrs-2 mR-3">
<i class="ti-menu"></i>
</button>
<!-- <button type="button" class="btn bgc-white bdrs-2 mR-3 cur-p">
<i class="ti-tag"></i>
</button> -->
<button type="button" class="btn bg-body bdrs-2 mR-3" onclick="javascript:loadTicketItems();">
<i class="ti-reload"></i>
</button>
<button type="button" class="btn bg-body bdrs-2 mR-3" onclick="javascript: toggleComplexItems();">
<i class="ti-layout-media-left"></i>
</button>
<ul class="dropdown-menu fsz-sm" aria-labelledby="btnGroupDrop1">
<li>
<a href="" class="d-b td-n pY-5 pX-10 bgcH-grey-100 c-grey-700">
<i class="ti-trash mR-10"></i>
<span>Delete</span>
</a>
</li>
<li>
<a href="" class="d-b td-n pY-5 pX-10 bgcH-grey-100 c-grey-700">
<i class="ti-alert mR-10"></i>
<span>Mark as Spam</span>
</a>
</li>
<li>
<a href="" class="d-b td-n pY-5 pX-10 bgcH-grey-100 c-grey-700">
<i class="ti-star mR-10"></i>
<span>Star</span>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="peer">
<div class="btn-group" role="group">
<button type="button" class="fsz-xs btn bgc-white bdrs-2 mR-3 cur-p">
<i class="ti-angle-left"></i>
</button>
<button type="button" class="fsz-xs btn bgc-white bdrs-2 mR-3 cur-p">
<i class="ti-angle-right"></i>
</button>
<!-- <div class="peer me-2">
<div class="btn-group" role="group">
</div>
</div> -->
<div class="peer ms-auto">
<div class="btn-group bg-body mX-3" role="group">
<button type="button" id="ticketItemsPrevPage" class="btn bg-body bdrs-2" onclick="javascript: changeItemsPage(false);" disabled>
<i class="ti-angle-left"></i>
</button>
<div id="paginationCounts" class="bg-body pX-3 small d-flex ai-c">
<span class="current">-</span>/<span class="total">-</span>
</div>
<button type="button" id="ticketItemsNextPage" class="btn bg-body bdrs-2" onclick="javascript: changeItemsPage(true);" disabled>
<i class="ti-angle-right align-center"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="layer w-100">
<div class="bdT bdB">
<input type="text" class="form-control m-0 bdw-0 pY-15 pX-20 bdrs-0" placeholder="Search...">
<div class="bdT bdB peers d-flex flex-nowrap">
<label for="searchTickets" class="peer my-auto mX-15">
<i class="ti-search"></i>
</label>
<input type="text" id="searchTickets" class="form-control m-0 bdw-0 pY-15 pR-20 pL-0 bdrs-0 peer-greed shadow-none" placeholder="Search...">
<label for="searchTickets" id="ticketCounts" class="peer my-auto mX-15 small">
<span class="current"></span>/<span class="total"></span>
</label>
</div>
</div>
<div id="ticketsContainer" class="layer w-100 fxg-1 scrollable pos-r"></div>
<div id="ticketsContainer" class="layer w-100 fxg-1 scrollable pos-r ov-h">
<div class="content"></div>
<div class="none-found p-20" style="display: none;">
<div class="pos-a top-50 start-50 translate-middle text-center">
<div class="fs-1 fw-bolder">404</div>
<div class="fs-4 fw-bold text-body-tertiary">No Tickets Found</div>
</div>
</div>
<div class="loading bg-body-tertiary h-100" style="display: none;">
{% for i in "x"|rjust:"3" %}
<div class="email-list-item peers fxw-nw p-20 bdB placeholder-glow" >
<div class="peer mR-10">
<span class="placeholder w-2r h-2r bdrs-50p me-2"></span>
</div>
<div class="peer peer-greed ov-h">
<div class="peers ai-c">
<div class="peer peer-greed">
<span class="placeholder col-5 rounded"></span>
</div>
<div class="peer peer-greed d-flex jc-fe">
<span class="placeholder col-4 rounded"></span>
</div>
</div>
<span class="placeholder rounded col-7 mT-10"></span>
<div class="row">
<span class="col-1"></span>
<span class="placeholder rounded col-7 mT-10"></span>
</div>
</div>
</div>
<div class="email-list-item peers fxw-nw p-20 bdB placeholder-glow" >
<div class="peer mR-10">
<span class="placeholder w-2r h-2r bdrs-50p me-2"></span>
</div>
<div class="peer peer-greed ov-h">
<div class="peers ai-c">
<div class="peer peer-greed">
<span class="placeholder col-6 rounded"></span>
</div>
<div class="peer peer-greed d-flex jc-fe">
<span class="placeholder col-4 rounded"></span>
</div>
</div>
<span class="placeholder rounded col-8 mT-10"></span>
<span class="placeholder rounded col-7 mT-10"></span>
</div>
</div>
<div class="email-list-item peers fxw-nw p-20 bdB placeholder-glow" >
<div class="peer mR-10">
<span class="placeholder w-2r h-2r bdrs-50p me-2"></span>
</div>
<div class="peer peer-greed ov-h">
<div class="peers ai-c">
<div class="peer peer-greed">
<span class="placeholder col-5 rounded"></span>
</div>
<div class="peer peer-greed d-flex jc-fe">
<span class="placeholder col-4 rounded"></span>
</div>
</div>
<div class="row">
<span class="col-1"></span>
<span class="placeholder rounded col-7 mT-10"></span>
</div>
<span class="placeholder rounded col-7 mT-10"></span>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="email-content h-100">
<div class="email-content h-100 bg-body">
<div class="h-100 scrollable pos-r">
<div class="bgc-grey-100 peers ai-c jc-sb p-20 fxw-nw d-n@md+">
<div class="peer">
<div class="btn-group" role="group">
<button type="button" class="back-to-mailbox btn bgc-white bdrs-2 mR-3 cur-p">
<i class="ti-angle-left"></i>
</button>
<button type="button" class="btn bgc-white bdrs-2 mR-3 cur-p">
<i class="ti-folder"></i>
</button>
<button type="button" class="btn bgc-white bdrs-2 mR-3 cur-p">
<i class="ti-tag"></i>
</button>
<div class="btn-group" role="group">
<button id="btnGroupDrop1" type="button" class="btn cur-p bgc-white no-after dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="ti-more-alt"></i>
</button>
<ul class="dropdown-menu fsz-sm" aria-labelledby="btnGroupDrop1">
<li>
<a href="" class="d-b td-n pY-5 pX-10 bgcH-grey-100 c-grey-700">
<i class="ti-trash mR-10"></i>
<span>Delete</span>
</a>
</li>
<li>
<a href="" class="d-b td-n pY-5 pX-10 bgcH-grey-100 c-grey-700">
<i class="ti-alert mR-10"></i>
<span>Mark as Spam</span>
</a>
</li>
<li>
<a href="" class="d-b td-n pY-5 pX-10 bgcH-grey-100 c-grey-700">
<i class="ti-star mR-10"></i>
<span>Star</span>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="peer">
<div class="btn-group" role="group">
<button type="button" class="fsz-xs btn bgc-white bdrs-2 mR-3 cur-p">
<i class="ti-angle-left"></i>
</button>
<button type="button" class="fsz-xs btn bgc-white bdrs-2 mR-3 cur-p">
<i class="ti-angle-right"></i>
</button>
</div>
</div>
</div>
<div class="email-content-wrapper">
<!-- Header -->
<div class="peers ai-c jc-sb pX-40 pY-30">
<div class="peers peer-greed">
<div class="peer mR-20">
<img id="ticketAuthorImg" class="bdrs-50p w-3r h-3r" alt="" src="" style="display: none; object-fit: cover;">
</div>
<div class="bg-body-tertiary peers ai-c jc-sb p-20 fxw-nw d-n@md+">
<div class="peer">
<small id="ticketTimestamp"></small>
<h5 id="ticketAuthor" class="c-grey-900 mB-5"></h5>
<div id="ticketBadges" style="display: none;"></div>
</div>
<div class="btn-group" role="group">
<button type="button" class="back-to-mailbox btn bg-body bdrs-2 mR-3">
<i class="ti-angle-left"></i>
</button>
<button type="button" class="btn bg-body bdrs-2 mR-3" onclick="javascript:reloadCurrentTicket();">
<i class="ti-reload"></i>
</button>
<button type="button" class="btn bg-body bdrs-2 mR-3">
<i class="ti-more-alt"></i>
</button>
</div>
</div>
<div class="peer">
<div class="btn-group" role="group">
<button id="btnGroupDrop2" class="btn btn-danger c-white bdrs-50p p-15 lh-0" style="display: none;" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="ti-menu"></i>
<button type="button" class="btn bg-body bdrs-2 mR-3" onclick="javascript:changeTicket(false);">
<i class="ti-angle-left"></i>
</button>
<button type="button" class="btn bg-body bdrs-2 mR-3" onclick="javascript:changeTicket(true);">
<i class="ti-angle-right"></i>
</button>
<ul class="dropdown-menu fsz-sm" aria-labelledby="btnGroupDrop2">
test
</ul>
</div>
</div>
</div>
<div id="ticketContent" class="email-content-wrapper">
<div class="content"></div>
<div class="loading" style="display: none;">
<!-- Content -->
<div class="bdT pX-40 pY-30">
<!-- Header -->
<div class="ticket-content placeholder-glow">
<div class="peers ai-c jc-sb pX-40 pY-30">
<div class="peers peer-greed">
<div class="peer mR-20">
<span class="ticket-content-icon placeholder me-2"></span>
</div>
<div class="peer-greed">
<div >
<span class="placeholder rounded col-4"></span>
</div>
<div class="mY-5">
<span class="placeholder rounded col-5"></span>
</div>
<div class="row g-0 ">
{% for i in "x"|rjust:"12" %}
<div class="col pe-3">
<div class="placeholder rounded w-100"></div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<h4 id="ticketTitle"></h4>
<div id="ticketDesc"></div>
<!-- <h4>Title of this email goes here</h4>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam
</p> -->
</div>
<!-- Content -->
<div class="bdT pX-40 pY-30">
<span class="placeholder rounded col-6 mB-30"></span>
<div class="row g-3">
<div class="placeholder rounded col-12 px-3"></div>
<div class="placeholder rounded col-7"></div>
<div class="col-1"></div>
<div class="placeholder rounded col-4"></div>
<div class="placeholder rounded col-3"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -349,7 +367,7 @@
</div>
<div class="mb-3">
<label for="newDesc" class="form-label">Description</label>
<div id="newDesc" class="form-control"></div>
<div id="newDesc" class="form-control"></div>
<small class="text-muted">Describe your issue in detail here.</small>
<!-- class="form-control" -->
</div>
@ -357,16 +375,16 @@
<label for="newPriority" class="form-label">Priority</label>
<select name="newPriority" id="newPriority" class="select-2">
{% for priority in priorities %}
<option value="{{ priority.id }}">{{ priority.title }}</option>
<option value="{{ priority.uuid }}">{{ priority.title }}</option>
{% endfor %}
</select>
<small class="text-muted">How important is this ticket?</small>
</div>
<div class="mb-3">
<label for="newTagss" class="form-label">Tags</label>
<label for="newTags" class="form-label">Tags</label>
<select name="newTags" id="newTags" class="select-2" multiple="multiple">
{% for tag in tags %}
<option value="{{ tag.id }}">{{ tag.title }}</option>
<option value="{{ tag.uuid }}">{{ tag.title }}</option>
{% endfor %}
</select>
<small class="text-muted">Use tags to categorize this ticket.</small>
@ -383,208 +401,87 @@
{% endblock content %}
<!-- Specific Page JS goes HERE -->
{% block javascripts %}
<script>
var displayedTicketID = -1;
$(document).ready(function() {
// $(".email-list-item").on("click", function() {
// displayTicket(this);
// });
ClassicEditor
.create( document.getElementById("newDesc"), {})
.catch( error => {
console.error(error)
});
loadAllTickets();
});
$("#ticketModal form").on("submit", function(event) {
event.preventDefault();
$.ajax({
url: "{% url 'ticket-new' %}",
type: "POST",
dataType: "json",
data: {
csrfmiddlewaretoken: "{{ csrf_token }}",
}
});
});
function getOrdinalSuffix(day) {
if (day >= 11 && day <= 13) {
return day + 'th';
} else {
switch (day % 10) {
case 1: return day + 'st';
case 2: return day + 'nd';
case 3: return day + 'rd';
default: return day + 'th';
}
}
}
function loadAllTickets() {
$("#ticketsContainer").empty();
$.ajax({
url: "{% url 'ticket-getmany' %}",
type: "POST",
dataType: "json",
data: {
csrfmiddlewaretoken: "{{ csrf_token }}",
filters: {}
},
success: function(data) {
console.log(JSON.stringify(data, null, 4))
data.tickets.forEach(function(ticket) {
var timestamp = new Date(ticket.timestamp);
var formattedTime;
if (ticket.was_yesterday) {
var day = getOrdinalSuffix(timestamp.getDate());
var month = timestamp.toLocaleString('en-GB', { month: 'short' });
var year = timestamp.toLocaleString('en-GB', { year: 'numeric' });
var time = timestamp.toLocaleString('en-GB', { hour: 'numeric', minute: 'numeric' });
// Formatting the final result
var formattedTime = time + ', ' + day + ' ' + month + ' ' + year;
}
else {
var hours = timestamp.getUTCHours();
var minutes = timestamp.getUTCMinutes();
formattedTime = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0');
}
if (ticket.is_edited) {
formattedTime += " • edited";
}
var item = $(`
<div class="email-list-item peers fxw-nw p-20 bdB bgcH-grey-100 cur-p" data-ticket-id="${ticket.id}" data-author-icon="${ticket.author.icon}">
<div class="peer mR-10">
<img src="${ticket.author.icon}" alt="" class="w-2r h-2r bdrs-50p me-2" style="object-fit: cover;">
</div>
<div class="peer peer-greed ov-h">
<div class="peers ai-c">
<div class="peer peer-greed">
<h6 class="ticket-author">${ticket.author.forename} ${ticket.author.surname}</h6>
</div>
<div class="peer">
<small class="ticket-timestamp">${formattedTime}</small>
</div>
</div>
<h5 class="fsz-def tt-c c-grey-900 ticket-title">${ticket.title}</h5>
<span class="whs-nw w-100 ov-h tov-e d-b ticket-desc">${ticket.description}</span>
</div>
</div>
`);
$("#ticketsContainer").append(item);
});
$(".email-list-item").on("click", function() {
displayTicket(this);
});
},
error: function(data) {
alert(JSON.stringify(data, null, 4))
}
});
}
function displayTicket(ticketElement) {
ticket = $(ticketElement);
ticketID = ticket.data("ticket-id");
$(".back-to-mailbox").off("click").on("click", function(event) {
event.preventDefault();
$('.email-content').toggleClass('open');
displayTicket(ticketElement);
});
$("#ticketTitle").text("")
$("#ticketDesc").empty();
$("#ticketAuthor").text("");
$("#ticketAuthorImg").hide();
$("#ticketAuthorImg").prop("src", "");
$("#ticketTimestamp").text("");
$("#btnGroupDrop2").hide();
$("#ticketBadges").empty().hide();
if (displayedTicketID === ticketID) {
displayedTicketID = -1;
return;
}
displayedTicketID = ticketID;
$.ajax({
url: `{% url 'ticket-getone' %}`,
type: 'POST',
dataType: 'json',
data: {
csrfmiddlewaretoken: '{{ csrf_token }}',
ticket_id: ticketID
},
success: function (data) {
console.log(JSON.stringify(data, null, 4));
var ticket = data.ticket;
var author = ticket.author;
var department = author.department;
var priority = ticket.priority;
$("#ticketTitle").text(ticket.title);
$("#ticketDesc").append($(`<div class="w-100">${ticket.description}</div>`));
$("#ticketAuthor").text(`${author.forename} ${author.surname}`);
$("#ticketAuthorImg").show();
$("#ticketAuthorImg").prop("src", author.icon);
$("#btnGroupDrop2").show();
$("#ticketBadges").show();
$("#ticketBadges").append($(`<div class="badge me-1" style="color: ${priority.colour}; background-color: ${priority.backgroundcolour};">${priority.title} Priority <i class="ti-control-record "></i></div>`));
if (department != null) {
$("#ticketBadges").append($(`<div class="badge bgc-deep-purple-500 me-1">${department.title}</div>`));
}
ticket.tags.forEach(function(tag) {
$("#ticketBadges").append($(`<div class="badge me-1" style="color: ${tag.colour}; background-color: ${tag.backgroundcolour};">${tag.title} <i class="ti-tag"></i></div>`));
});
// timestamp
var timestamp = new Date(ticket.timestamp);
var formattedTime;
if (ticket.was_yesterday) {
var options = { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' };
formattedTime = timestamp.toLocaleDateString('en-GB', options);
}
else {
var hours = timestamp.getUTCHours();
var minutes = timestamp.getUTCMinutes();
formattedTime = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0');
}
if (ticket.is_edited) {
formattedTime += " • edited";
}
$("#ticketTimestamp").text(formattedTime);
},
error: function(message) {
alert(JSON.stringify(message, null, 4));
}
});
}
<!-- Ticket Item Template -->
<script id="ticketItemTemplate" type="text/template">
<div class="ticket-item fxw-nw bdB peers fxw-nw p-20 w-100" data-uuid="-1">
<div class="ticket-item-complex peer mR-20 d-flex flex-column align-items-center align-self-stretch pos-r">
<img src="" alt="" class="ticket-item-icon">
<div class="mt-auto">
<div class="ticket-item-department badge rounded mb-2" data-bs-toggle="tooltip">
<i class="fa fa-users"></i>
</div>
<div class="ticket-item-priority badge rounded" data-bs-toggle="tooltip">
<i class="fa fa-folder"></i>
</div>
</div>
</div>
<div class="peer peer-greed ov-h">
<div class="peers ai-c mb-2">
<div class="peer peer-greed">
<h6 class="ticket-item-author"></h6>
</div>
<div class="peer">
<small class="ticket-item-datetime"></small>
</div>
</div>
<h5 class="ticket-item-title mb-0"></h5>
<div class="ticket-item-desc mt-2"></div>
<div class="peers">
<div class="ticket-item-tags peer d-flex flex-wrap mw-100"></div>
</div>
</div>
</div>
</script>
<!-- Ticket Content Template -->
<script id="ticketContentTemplate" type="text/template">
<!-- Header -->
<div class="ticket-content">
<div class="peers ai-c jc-sb pX-40 pY-30">
<div class="peers peer-greed">
<div class="peer mR-20">
<img class="ticket-content-icon" src="" alt="">
</div>
<div class="peer">
<!-- <small class="ticket-content-datetime"></small> -->
<h5 class="ticket-content-author mb-0"></h5>
<div class="peers mt-2">
<div class="ticket-content-department peer badge bgc-orange-100 c-orange-700" data-bs-toggle="tooltip">
<i class="fa fa-users"></i>
</div>
<div class="ticket-content-priority peer badge rounded" data-bs-toggle="tooltip">
<i class="fa fa-folder"></i>
</div>
</div>
<div class="ticket-content-badges"></div>
</div>
</div>
</div>
<!-- Content -->
<div class="bdT pX-40 pY-30">
<h4 class="ticket-content-title"></h4>
<div class="ticket-content-desc w-100"></div>
</div>
</div>
</script>
<!-- Ticket Content Badge Template -->
<script id="ticketContentBadgeTemplate" type=text/template>
<div class="ticket-content-badge badge rounded mt-2 me-2">
<span class="ticket-content-badge-text"></span>
<i class="ticket-content-badge-icon"></i>
</div>
</script>
<!-- Define Variables -->
<script>
const URL_Tickets = "{% url 'api:tickets' %}";
const URL_NewTicket = "{% url 'ticket-new' %}";
const URL_FilterCounts = "{% url 'api:filter-counts' %}";
const CSRFMiddlewareToken = "{{ csrf_token }}";
const CurrentUserID = "{{ request.user.uuid }}";
</script>
<script src="{% static '/js/tickets.js' %}"></script>
{% endblock javascripts %}

View File

@ -1,4 +1,4 @@
<footer class="bdT ta-c p-30 lh-0 fsz-sm c-grey-600">
<footer class="bdT ta-c p-30 lh-0 fsz-sm c-grey-600 bg-body">
<span>
&copy; Designed by Colorlib.
</span>

View File

@ -1,4 +1,4 @@
<div class="header navbar">
<div class="header navbar bg-body">
<div class="header-container">
<ul class="nav-left">
<li>
@ -17,6 +17,11 @@
</li>
</ul>
<ul class="nav-right">
<li>
<a href="" id="themeToggle" data-bs-toggle="dropdown">
<i class="ti-shine"></i>
</a>
</li>
<li class="notifications dropdown">
<!-- <span class="counter bgc-red">3</span> -->
<a href="" class="dropdown-toggle no-after" data-bs-toggle="dropdown">
@ -106,7 +111,7 @@
<img class="w-2r h-2r bdrs-50p" src="{{ request.user.icon.url }}" style="object-fit: cover;" alt="">
</div>
<div class="peer">
<span class="fsz-sm c-grey-900">{{ request.user.formal_fullname }}</span>
<span class="fsz-sm">{{ request.user.formal_fullname }}</span>
</div>
</a>
<ul class="dropdown-menu fsz-sm">

View File

@ -1,9 +1,17 @@
<script src="{{ ASSETS_ROOT }}/js/jquery-3.6.0.min.js"></script>
{% load static %}
<script src="{{ ASSETS_ROOT }}/js/jquery.dataTables.min.js"></script>
<script src="{% static '/js/jquery-3.6.0.min.js' %}"></script>
<script src="{{ ASSETS_ROOT }}/js/ckeditor.js"></script>
<script src="{% static '/js/jquery.dataTables.min.js' %}"></script>
<script src="{{ ASSETS_ROOT }}/js/select2.min.js"></script>
<script src="{% static '/js/ckeditor.js' %}"></script>
<script src="{{ ASSETS_ROOT }}/js/index.js"></script>
<script src="{% static '/js/select2.min.js' %}"></script>
<script src="{% static '/js/perfectscrollbar.js' %}"></script>
<script src="{% static '/js/bootstrap.bundle.min.js' %}"></script>
<script src="{% static '/js/index.js' %}"></script>
<script src="{% static '/js/base.js' %}"></script>

View File

@ -1,19 +1,19 @@
<div class="sidebar">
{% load static %}
<div class="sidebar bg-body">
<div class="sidebar-inner">
<!-- ### $Sidebar Header ### -->
<div class="sidebar-logo">
<div class="peers ai-c fxw-nw">
<div class="peer peer-greed">
<a class="sidebar-link td-n" href="/index.html">
<a class="sidebar-link td-n" href="/">
<div class="peers ai-c fxw-nw">
<div class="peer">
<div class="logo">
<img src="{{ ASSETS_ROOT }}/images/logo.png" alt="">
<img src="{% static '/images/logo.png' %}" alt="">
</div>
</div>
<div class="peer peer-greed">
<h5 class="lh-1 mB-0 logo-text">Adminator</h5>
<h5 class="lh-1 mB-0 text-body-tertiary">Adminator</h5>
</div>
</div>
</a>
@ -35,7 +35,7 @@
<span class="icon-holder">
<i class="c-blue-500 ti-home"></i>
</span>
<span class="title">New Dashboard</span>
<span class="title">Admin Dashboard</span>
<!-- ti-comment-alt -->
</a>
</li>
@ -47,10 +47,10 @@
<span class="title">Tickets</span>
</a>
</li>
<li class="nav-item">
<!-- <li class="nav-item">
<hr class="bgc-grey-500">
</li>
<li class="nav-item">
</li> -->
<!-- <li class="nav-item">
<a class="sidebar-link" href="/index.html">
<span class="icon-holder">
<i class="c-blue-500 ti-home"></i>
@ -221,7 +221,7 @@
</span>
<span class="title">Download</span>
</a>
</li>
</li> -->
</ul>
</div>

View File

@ -1,4 +1,6 @@
<!DOCTYPE html>
{% load static %}
<html>
<head>
<meta charset="utf-8">
@ -7,14 +9,16 @@
Django Adminator - {% block title %}{% endblock %} | AppSeed
</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/css/jquery.dataTables.min.css"
integrity="sha512-1k7mWiTNoyx2XtmI96o+hdjP8nn0f3Z2N4oF/9ZZRgijyV4omsKOXEnqL1gKQNPy2MTSP9rIEWGcH/CInulptA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link type="text/css" href="{{ ASSETS_ROOT }}/css/index.css" rel="stylesheet">
<link type="text/css" rel="stylesheet" href="{% static '/css/bootstrap.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/colours.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/datepicker.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/fontawesome.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/themify-icons.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/scrollbar.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/adminator.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/jquery.dataTables.min.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/select2.min.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/index.css' %}" />
<!-- Specific Page CSS goes HERE -->
{% block stylesheets %}{% endblock stylesheets %}
@ -22,7 +26,7 @@
</head>
<body class="app">
<div id='loader'>
<div id='loader' class="bg-body">
<div class="spinner"></div>
</div>

View File

@ -1,4 +1,6 @@
<!DOCTYPE html>
{% load static %}
<html>
<head>
<meta charset="utf-8">
@ -7,14 +9,16 @@
Django Adminator - {% block title %}{% endblock %} | AppSeed
</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/css/jquery.dataTables.min.css"
integrity="sha512-1k7mWiTNoyx2XtmI96o+hdjP8nn0f3Z2N4oF/9ZZRgijyV4omsKOXEnqL1gKQNPy2MTSP9rIEWGcH/CInulptA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link type="text/css" href="{{ ASSETS_ROOT }}/css/index.css" rel="stylesheet">
<link type="text/css" rel="stylesheet" href="{% static '/css/bootstrap.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/colours.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/datepicker.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/fontawesome.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/themify-icons.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/scrollbar.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/adminator.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/jquery.dataTables.min.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/select2.min.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/index.css' %}" />
<!-- Specific Page CSS goes HERE -->
{% block stylesheets %}{% endblock stylesheets %}

View File

@ -1,4 +1,6 @@
<!DOCTYPE html>
{% load static %}
<html>
<head>
<meta charset="utf-8">
@ -7,15 +9,22 @@
Django Adminator - {% block title %}{% endblock %}
</title>
<link type="text/css" rel="stylesheet" href="{{ ASSETS_ROOT }}/css/jquery.dataTables.min.css" />
<link type="text/css" rel="stylesheet" href="{{ ASSETS_ROOT }}/css/index.css" />
<link type="text/css" rel="stylesheet" href="{{ ASSETS_ROOT }}/css/select2.min.css" />
<link type="text/css" rel="stylesheet" href="{% static '/css/bootstrap.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/colours.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/datepicker.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/fontawesome.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/themify-icons.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/perfectscrollbar.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/adminator.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/jquery.dataTables.min.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/select2.min.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static '/css/index.css' %}" />
<!-- Specific Page CSS goes HERE -->
{% block stylesheets %}{% endblock stylesheets %}
</head>
<body class="app">
<body class="app" data-bs-theme="light">
<!-- @TOC -->
<!-- =================================================== -->
@ -33,7 +42,7 @@
<!-- @Page Loader -->
<!-- =================================================== -->
<div id='loader'>
<div id='loader' class="bg-body">
<div class="spinner"></div>
</div>

View File

@ -1,31 +1,26 @@
# -*- encoding: utf-8 -*-
import os, environ
from pathlib import Path
env = environ.Env(
# set casting, default value
DEBUG=(bool, True)
)
from django.utils import timezone
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
CORE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# BASE_DIR is the root of the project, all paths should be constructed from it using pathlib
BASE_DIR = Path(__file__).parent.parent
# Take environment variables from .env file
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
# Create an environment and read variables from .env file
env = environ.Env(DEBUG=(bool, True))
environ.Env.read_env(BASE_DIR / ".env")
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY', default='S#perS3crEt_007')
# SECURITY WARNING: This is sensitive data, keep secure!
SECRET_KEY = env('SECRET_KEY', default="unsecure-default-secret-key")
# SECURITY WARNING: don't run with debug turned on in production!
# SECURITY WARNING: Must be 'False' in production!
DEBUG = env('DEBUG')
# Assets Management
ASSETS_ROOT = os.getenv('ASSETS_ROOT', '/static/assets')
# load production server from .env
ALLOWED_HOSTS = ['localhost', 'localhost:85', '127.0.0.1', '192.168.0.19', env('SERVER', default='127.0.0.1') ]
CSRF_TRUSTED_ORIGINS = ['http://localhost:85', 'http://127.0.0.1', 'https://' + env('SERVER', default='127.0.0.1') ]
# Hosts and Origins that the server host must be within.
ALLOWED_HOSTS = ["localhost", "127.0.0.1", env("HOST", default="127.0.0.1")]
CSRF_TRUSTED_ORIGINS = ["http://localhost", "http://127.0.0.1", "https://" + env("HOST", default="127.0.0.1")]
# Application definition
@ -36,7 +31,11 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'apps.home', # Enable the inner home (home)
'rest_framework',
"rest_framework.authtoken",
"django_filters",
'apps.api',
'apps.home',
'apps.authentication'
]
@ -52,14 +51,18 @@ MIDDLEWARE = [
]
ROOT_URLCONF = 'core.urls'
LOGIN_REDIRECT_URL = "home" # Route defined in home/urls.py
LOGOUT_REDIRECT_URL = "home" # Route defined in home/urls.py
TEMPLATE_DIR = os.path.join(CORE_DIR, "apps/templates") # ROOT dir for templates
APPEND_SLASH = True
LOGIN_URL = "/login/"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
AUTH_USER_MODEL = "authentication.User"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [TEMPLATE_DIR],
'DIRS': [BASE_DIR / "apps/templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -67,7 +70,6 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'apps.context_processors.cfg_assets_root',
],
},
},
@ -75,8 +77,9 @@ TEMPLATES = [
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
if os.environ.get('DB_ENGINE') and os.environ.get('DB_ENGINE') == "mysql":
DATABASES = {
@ -98,7 +101,7 @@ else:
}
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
@ -115,8 +118,66 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# Logging
# https://docs.djangoproject.com/en/5.0/topics/logging/
LOGGING_DIR = BASE_DIR / "logs"
LOGGING_DIR.mkdir(exist_ok=True)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
# 'file': {
# 'level': 'DEBUG',
# 'class': 'logging.FileHandler',
# 'filename': LOGGING_DIR / f'{timezone.now()}.log',
# "formatter": "verbose",
# },
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
"formatter": "verbose"
},
"timed_file": {
"level": "DEBUG",
"class": "logging.handlers.TimedRotatingFileHandler",
"when": "D",
"interval": 1,
"backupCount": 3,
"encoding": "UTF-8",
"filename": LOGGING_DIR / "debug.log",
"formatter": "verbose"
}
},
'loggers': {
"apps": {
"handlers": ["timed_file", "console"],
"level": "DEBUG",
"propagate": True
},
"django": {
"handlers": ["timed_file", "console"],
"level": "INFO",
"propagate": True
},
"django.request": {
"handlers": ["timed_file", "console"],
"level": "DEBUG",
"propagate": True
}
},
"formatters": {
"verbose": {
"format": "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
}
}
}
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'en-gb'
@ -124,33 +185,34 @@ TIME_ZONE = 'Europe/London'
USE_I18N = True
USE_L10N = True
USE_TZ = True
#############################################################
# SRC: https://devcenter.heroku.com/articles/django-assets
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_ROOT = os.path.join(CORE_DIR, 'staticfiles')
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATIC_URL = '/static/'
# Extra places for collectstatic to find static files.
STATICFILES_DIRS = (
os.path.join(CORE_DIR, 'apps/static'),
BASE_DIR / 'apps/static',
)
# Media Files
MEDIA_ROOT = os.path.join(CORE_DIR, 'media')
MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_URL = '/media/'
#############################################################
#############################################################
# If route isnt found, try again with appended slash
APPEND_SLASH = True
# Django Rest Framework
# https://www.django-rest-framework.org/
AUTH_USER_MODEL = "authentication.User"
LOGIN_URL = "/login/"
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day'
}
}

View File

@ -6,7 +6,8 @@ from django.conf.urls.static import static
from django.urls import path, include # add this
urlpatterns = [
path('admin/', admin.site.urls), # Django admin route
path('admin/', admin.site.urls),
path("api/", include(("apps.api.urls", "apps.api"), namespace="api")),
# ADD NEW Routes HERE
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),

BIN
examples/dark.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
examples/light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

View File

@ -1,7 +1,4 @@
#!/usr/bin/env python
"""
Copyright (c) 2019 - present AppSeed.us
"""
import os
import sys

View File

@ -4,6 +4,9 @@ bleach==6.1.0
dj-database-url==0.5.0
Django==3.2.16
django-environ==0.8.1
django-filter==23.5
django-rest-framework==0.1.0
djangorestframework==3.14.0
gunicorn==20.1.0
pillow==10.2.0
pycodestyle==2.8.0

Some files were not shown because too many files have changed in this diff Show More