emile 11 months ago
parent 48bf694fe2
commit 50b5a695ef

BIN
.DS_Store vendored

Binary file not shown.

Binary file not shown.

@ -0,0 +1,11 @@
from django.contrib import admin
from .models import *
# Register your models here.
admin.site.register(ChatRoom)
admin.site.register(ChatMember)
admin.site.register(ChatProject)
admin.site.register(ChatMessage)
admin.site.register(ChatMessageAttachment)
admin.site.register(ChatMessageReaction)
admin.site.register(ChatMessageSeen)

@ -0,0 +1,6 @@
from django.apps import AppConfig
class OsichatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'osichat'

@ -0,0 +1,79 @@
# Generated by Django 4.2.5 on 2024-07-16 10:34
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('osinacore', '0097_remove_status_date_remove_status_time'),
]
operations = [
migrations.CreateModel(
name='ChatMessage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TimeField(blank=True, null=True)),
('date_sent', models.DateTimeField()),
('member', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='ChatRoom',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('date_created', models.DateTimeField()),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='ChatProject',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('public', models.BooleanField()),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='osinacore.project')),
('room', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='osichat.chatroom')),
],
),
migrations.CreateModel(
name='ChatMessageSeen',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='osichat.chatmessage')),
],
),
migrations.CreateModel(
name='ChatMessageReaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reaction', models.CharField(max_length=200)),
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='osichat.chatmessage')),
],
),
migrations.CreateModel(
name='ChatMessageAttachment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('attachment', models.TextField()),
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='osichat.chatmessage')),
],
),
migrations.CreateModel(
name='ChatMember',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_joined', models.DateTimeField()),
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='osichat.chatroom')),
],
),
]

@ -0,0 +1,42 @@
from django.db import models
from osinacore.models import *
# Create your models here.
class ChatRoom(models.Model):
name = models.CharField(max_length=50)
created_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
date_created = models.DateTimeField()
class ChatMember(models.Model):
member = models.ForeignKey(User, on_delete=models.CASCADE)
room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE)
date_joined = models.DateTimeField()
class ChatProject(models.Model):
room = models.OneToOneField(ChatRoom, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE)
public = models.BooleanField()
class ChatMessage(models.Model):
member = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
content = models.TimeField(null=True, blank=True)
date_sent = models.DateTimeField()
class ChatMessageAttachment(models.Model):
message = models.ForeignKey(ChatMessage, on_delete=models.CASCADE)
attachment = models.TextField()
class ChatMessageReaction(models.Model):
message = models.ForeignKey(ChatMessage, on_delete=models.CASCADE)
member = models.ForeignKey(User, on_delete=models.CASCADE)
reaction = models.CharField(max_length=200)
class ChatMessageSeen(models.Model):
message = models.ForeignKey(ChatMessage, on_delete=models.CASCADE)
member = models.ForeignKey(User, on_delete=models.CASCADE)

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

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

@ -40,6 +40,7 @@ LOGIN_URL = 'signin'
# Application definition
INSTALLED_APPS = [
'osichat',
'daphne',
'support',
'rest_framework',

@ -63,7 +63,7 @@ function app(socket) {
function initializeWebSocket() {
const ticketId = document.getElementById('ticketId').textContent.trim();
const wsUrl = `wss://${window.location.host}/ws/ticketroom/${ticketId}/`;
const wsUrl = `ws://${window.location.host}/ws/ticketroom/${ticketId}/`;
const socket = new WebSocket(wsUrl);
socket.onopen = () => {
@ -118,12 +118,25 @@ function initializeWebSocket() {
button.classList.remove('hidden');
}
});
} else if (data.event_type === 'user_status') {
const toponlineUsersDiv = document.getElementById('top-online-users');
const fixedonlineUsersDiv = document.getElementById('fixed-online-users');
toponlineUsersDiv.innerHTML = data.html;
fixedonlineUsersDiv.innerHTML = data.html;
} else if (data.event_type === 'ticket_status') {
const ticketStatusDivs = document.querySelectorAll('.ticket-status');
ticketStatusDivs.forEach(div => {
div.innerHTML = data.html;
});
} else if (data.event_type === 'ticket_department') {
const ticketStatusDivs = document.querySelectorAll('.ticket-department');
ticketStatusDivs.forEach(div => {
div.innerHTML = data.html;
});
} else {
messagesDiv.insertAdjacentHTML('beforeend', data.html);
typingDiv.innerHTML = '';

@ -167,6 +167,32 @@ class TicketRoomConsumer(WebsocketConsumer):
'reaction': new_reaction.reaction if new_reaction else None
}))
def new_status_handler(self, event):
last_ticket_status = TicketStatus.objects.filter(ticket=self.ticket).last()
context = {
'last_ticket_status': last_ticket_status,
'new': True
}
html = render_to_string("details_templates/partials/ticket-status.html", context=context)
self.send(text_data=json.dumps({
'event_type': 'ticket_status',
'html': html
}))
def new_department_handler(self, event):
last_ticket_department = TicketDepartment.objects.filter(ticket=self.ticket).last()
context = {
'last_ticket_department': last_ticket_department,
'new': True
}
html = render_to_string("details_templates/partials/ticket-department.html", context=context)
self.send(text_data=json.dumps({
'event_type': 'ticket_department',
'html': html
}))
def modify_online_user(self):
connections = TicketConnection.objects.filter(ticket=self.ticket, terminated_at__isnull=True)
event = {

@ -165,6 +165,17 @@ class TicketDepartment(models.Model):
date_added = models.DateTimeField()
@receiver(post_save, sender=TicketDepartment)
def new_ticket_department_handler(sender, instance, created, **kwargs):
if created:
channel_layer = get_channel_layer()
event = {
'type': 'new_department_handler',
}
ticket_number = instance.ticket.ticket_number
async_to_sync(channel_layer.group_send)(ticket_number, event)
class TicketStaff(models.Model):
staff = models.ForeignKey(StaffProfile, on_delete=models.CASCADE)
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
@ -205,6 +216,19 @@ class TicketStatus(models.Model):
date_added = models.DateTimeField()
@receiver(post_save, sender=TicketStatus)
def new_ticket_status_handler(sender, instance, created, **kwargs):
if created:
channel_layer = get_channel_layer()
event = {
'type': 'new_status_handler',
}
ticket_number = instance.ticket.ticket_number
async_to_sync(channel_layer.group_send)(ticket_number, event)
class TicketTask(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
task = models.ForeignKey(Task, on_delete=models.CASCADE)

@ -1,4 +1,4 @@
<div id="messages" hx-swap-oob="beforeend">
<div id="messages">
<div class="fade-in-up">
{% include 'details_templates/partials/ticket-message.html' %}

@ -0,0 +1,11 @@
<span class="{% if new %} fade-in-up {% endif %}">{{last_ticket_department.department}}</span>
<style>
@keyframes fadeInAndUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0px); }
}
.fade-in-up {
animation: fadeInAndUp 0.6s ease;
}
</style>

@ -0,0 +1,30 @@
{% if last_ticket_status.status == 'Open' %}
<div class="flex justify-start items-center gap-1 {% if new %} fade-in-up {% endif %}">
<div class="w-[16px] h-[16px] rounded-full bg-green-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Opened by {{last_ticket_status.added_by.first_name}} on
{{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
</div>
{% elif last_ticket_status.status == 'Working On' %}
<div class="flex justify-start items-center gap-1 {% if new %} fade-in-up {% endif %}"">
<div class="w-[16px] h-[16px] rounded-full bg-yellow-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Updated to 'Working On' by
{{last_ticket_status.added_by.first_name}} on {{ last_ticket_status.date_added|date:"d F Y, h:i A" }}
</p>
</div>
{% elif last_ticket_status.status == 'Closed' %}
<div class="flex justify-start items-center gap-1 {% if new %} fade-in-up {% endif %}">
<div class="w-[16px] h-[16px] rounded-full bg-red-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Closed by {{last_ticket_status.added_by.first_name}} on
{{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
</div>
{% endif %}
<style>
@keyframes fadeInAndUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0px); }
}
.fade-in-up {
animation: fadeInAndUp 0.6s ease;
}
</style>

@ -21,8 +21,7 @@
<div class="w-full h-fit flex flex-col gap-2 bg-gray-100 shadow-md rounded-md px-3 py-3" id="ticketHeader">
<div class="w-full flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-5 md:mb-0">
<p class="text-secondosiblue text-[20px] flex items-center gap-1">Ticket <span
class="font-semibold">#{{ticket.ticket_number}}</span> - <span
class="font-semibold text-base">{{ticket.ticketdepartment_set.all.last.department}}</span></p>
class="font-semibold">#{{ticket.ticket_number}}</span> - <span class="ticket-department font-semibold text-base">{% include 'details_templates/partials/ticket-department.html' %}</span></p>
<div class="w-full s:w-fit flex flex-col s:flex-row justify-end items-center gap-2">
@ -58,29 +57,9 @@
</div>
</div>
{% if last_ticket_status.status == 'Open' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-green-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Opened by {{last_ticket_status.added_by.first_name}} on
{{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
</div>
{% elif last_ticket_status.status == 'Working On' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-yellow-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Updated to 'Working On' by
{{last_ticket_status.added_by.first_name}} on {{ last_ticket_status.date_added|date:"d F Y, h:i A" }}
</p>
</div>
{% elif last_ticket_status.status == 'Closed' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-red-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Closed by {{last_ticket_status.added_by.first_name}} on
{{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
<div class="ticket-status">
{% include 'details_templates/partials/ticket-status.html' %}
</div>
{% endif %}
<div class="w-full">
<p class="text-gray-500 font-light text-sm leading-7">{{ticket.description}}
</p>
@ -118,30 +97,13 @@
<div id="fixedTicketHeader" class="w-full flex flex-col gap-3 bg-gray-100 shadow-md rounded-b-md px-3 py-3 hidden z-10">
<div class="w-full flex flex-col md:flex-row justify-between items-start md:items-center gap-3">
<p class="text-secondosiblue text-[17px] flex items-center gap-1">Ticket <span
class="font-semibold">#{{ticket.ticket_number}}</span> - <span
class="font-semibold text-base">{{ticket.ticketdepartment_set.all.last.department}}</span>
class="font-semibold">#{{ticket.ticket_number}}</span> - <span class="ticket-department font-semibold text-base">{% include 'details_templates/partials/ticket-department.html' %}</span></span>
</p>
{% if last_ticket_status.status == 'Open' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-green-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Opened by {{last_ticket_status.added_by.first_name}} on
{{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
</div>
{% elif last_ticket_status.status == 'Working On' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-yellow-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Updated to 'Working On' by
{{last_ticket_status.added_by.first_name}} on {{ last_ticket_status.date_added|date:"d F Y, h:i A" }}
</p>
</div>
{% elif last_ticket_status.status == 'Closed' %}
<div class="flex justify-start items-center gap-1">
<div class="w-[16px] h-[16px] rounded-full bg-red-200 shadow-md"></div>
<p class="text-secondosiblue font-light">Closed by {{last_ticket_status.added_by.first_name}} on
{{ last_ticket_status.date_added|date:"d F Y, h:i A" }}</p>
<div class="ticket-status">
{% include 'details_templates/partials/ticket-status.html' %}
</div>
{% endif %}
</div>
<div class="w-full flex items-center gap-1">

@ -23,6 +23,7 @@ def ticket_room(request, ticket_number):
TicketRead.objects.create(ticket_update=update, user=request.user, read=True)
last_ticket_status = TicketStatus.objects.filter(ticket=ticket).last()
last_ticket_department = TicketDepartment.objects.filter(ticket=ticket).last()
connections = TicketConnection.objects.filter(terminated_at__isnull=True).order_by('-id')
@ -31,6 +32,7 @@ def ticket_room(request, ticket_number):
'ticket' : ticket,
'ticket_updates': ticket_updates,
'last_ticket_status': last_ticket_status,
'last_ticket_department': last_ticket_department,
'connections': connections,
}

Loading…
Cancel
Save