[UPDT] HELPDESK: Added load faqs to faq view

This commit is contained in:
Horilla
2025-05-21 11:16:47 +05:30
parent 3479708a70
commit 3164464f49
13 changed files with 1769 additions and 130 deletions

View File

@@ -261,8 +261,8 @@ class FAQCategory(HorillaModel):
class FAQ(HorillaModel):
question = models.CharField(max_length=255)
answer = models.TextField(max_length=255)
tags = models.ManyToManyField(Tags)
answer = models.TextField()
tags = models.ManyToManyField(Tags, blank=True)
category = models.ForeignKey(FAQCategory, on_delete=models.PROTECT)
company_id = models.ForeignKey(
Company, null=True, editable=False, on_delete=models.PROTECT

View File

@@ -47,6 +47,39 @@
</select>
{% endcomment %}
</div>
<div class="oh-accordion-meta__actions" onclick="event.stopPropagation()">
<div class="oh-dropdown" x-data="{open: false}">
<button
class="oh-btn oh-stop-prop oh-accordion-meta__btn"
@click="open = !open"
@click.outside="open = false"
>
{% trans "Actions" %}
<ion-icon
class="ms-2 oh-accordion-meta__btn-icon"
name="caret-down-outline"
></ion-icon>
</button>
<div
class="oh-dropdown__menu oh-dropdown__menu--right"
x-show="open"
>
<ul class="oh-dropdown__items">
<li class="oh-dropdown__item" role="button">
<a
class="oh-dropdown__link"
hx-get="{% url 'load-faqs' %}"
hx-target="#objectCreateModalTarget"
data-target="#objectCreateModal"
data-toggle="oh-modal-toggle"
>
{% trans "Load Faqs" %}
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="oh-main__titlebar oh-main__titlebar--right">
<form
method="post"
@@ -66,7 +99,7 @@
</form>
{% if perms.helpdesk.add_faqcategory %}
<div class="oh-main__titlebar-button-container">
<div class="oh-btn-group ml-2">
<div class="oh-btn-group m-0">
<div class="oh-dropdown" x-data="{open: false}">
<button
class="oh-btn oh-btn--secondary oh-btn--shadow"

View File

@@ -1,62 +1,92 @@
{% load i18n static %}
{% include 'filter_tags.html' %}
{% load i18n static %} {% include 'filter_tags.html' %}
<div class="oh-card mb-4">
<div class="oh-faq">
<ul class="oh-faq__items">
{% for faq in faqs %}
<li class="oh-faq__item fade-me-out" id="faqItem{{faq.id}}">
<div class="oh-faq__item-header icon-inner" onclick="show_answer(this)">
<div class="oh-faq__item-header__left icon-inner">
<span class="oh-faq__item-title icon-inner"> {{faq.question}} </span>
<ul class="oh-faq__tags">
{% for tag in faq.tags.all %}
<li class="oh-faq__tag text-light" style="background:{{tag.color}};">{{tag|capfirst}}</li>
{% endfor %}
</ul>
</div>
<div class="oh-faq__item-header__right">
{% if perms.helpdesk.change_faq %}
<button class="oh-btn oh-btn--sq oh-btn--transparent" title="{% trans 'Edit' %}"
data-toggle="oh-modal-toggle" data-target="#objectCreateModal"
hx-get="{% url 'faq-update' faq.id %}" hx-target="#objectCreateModalTarget"
onclick='event.stopPropagation()'>
<ion-icon name="create-outline"></ion-icon>
</button>
{% endif %}
{% if perms.helpdesk.delete_faq %}
<form hx-confirm="{% trans 'Are you sure you want to delete this FAQ?' %}"
hx-target="#faqItem{{faq.id}}" hx-post="{% url 'faq-delete' faq.id %}" class="w-50"
hx-on:click="event.stopPropagation();"
hx-on-htmx-after-request="setTimeout(() => {reloadMessage(this);},100);"
hx-swap="outerHTML swap:.5s">
{% csrf_token %}
<button type="submit" class="oh-btn oh-btn--sq oh-btn--danger-text oh-btn--transparent">
<ion-icon class="me-1 md hydrated" name="trash-outline" role="img"
aria-label="trash outline"></ion-icon>
</button>
</form>
{% endif %}
</div>
</div>
<div class="oh-faq__item-body">{{faq.answer}}</div>
</li>
{% empty %}
<div style="
height: 70vh;
display: flex;
align-items: center;
justify-content: center;
position:relative;
">
<div class="oh-404">
<img style="display: block; width: 150px; height: 150px; margin: 10px auto"
src="{% static 'images/ui/faq.png' %}" class="mb-4" alt="" />
<h3 style="font-size: 20px" class="oh-404__subtitle">
{% trans "There are no FAQs at the moment." %}
</h3>
</div>
</div>
{% endfor %}
</ul>
</div>
<div class="oh-faq">
<ul class="oh-faq__items">
{% for faq in faqs %}
<li class="oh-faq__item fade-me-out" id="faqItem{{faq.id}}">
<div
class="oh-faq__item-header icon-inner"
onclick="show_answer(this)"
>
<div class="oh-faq__item-header__left icon-inner">
<span class="oh-faq__item-title icon-inner">
{{faq.question}}
</span>
<ul class="oh-faq__tags">
{% for tag in faq.tags.all %}
<li
class="oh-faq__tag text-light"
style="background:{{tag.color}};"
>
{{tag|capfirst}}
</li>
{% endfor %}
</ul>
</div>
<div class="oh-faq__item-header__right">
{% if perms.helpdesk.change_faq %}
<button
class="oh-btn oh-btn--sq oh-btn--transparent"
title="{% trans 'Edit' %}"
data-toggle="oh-modal-toggle"
data-target="#objectCreateModal"
hx-get="{% url 'faq-update' faq.id %}"
hx-target="#objectCreateModalTarget"
onclick="event.stopPropagation()"
>
<ion-icon name="create-outline"></ion-icon>
</button>
{% endif %}
{% if perms.helpdesk.delete_faq %}
<form
hx-confirm="{% trans 'Are you sure you want to delete this FAQ?' %}"
hx-target="#faqItem{{faq.id}}"
hx-post="{% url 'faq-delete' faq.id %}"
class="w-50"
hx-on:click="event.stopPropagation();"
hx-on-htmx-after-request="setTimeout(() => {reloadMessage(this);},100);"
hx-swap="outerHTML swap:.5s"
>
{% csrf_token %}
<button
type="submit"
class="oh-btn oh-btn--sq oh-btn--danger-text oh-btn--transparent"
>
<ion-icon
class="me-1 md hydrated"
name="trash-outline"
role="img"
aria-label="trash outline"
></ion-icon>
</button>
</form>
{% endif %}
</div>
</div>
<div class="oh-faq__item-body">{{faq.answer}}</div>
</li>
{% empty %}
<div
style="
height: 70vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
"
>
<div class="oh-404">
<img
src="{% static 'images/ui/faq.png' %}"
class="mb-4 oh-404__image"
alt=""
/>
<h3 style="font-size: 20px" class="oh-404__subtitle">
{% trans "There are no FAQs at the moment." %}
</h3>
</div>
</div>
{% endfor %}
</ul>
</div>
</div>

View File

@@ -1,75 +1,82 @@
{% extends 'index.html' %} {% block content %} {% load static %} {% load i18n %}
<div id="faqContainer">
{% include 'helpdesk/faq/faq_nav.html'%}
<div class="oh-wrapper">
<div id="faqList">{% include "helpdesk/faq/faq_list.html" %}</div>
</div>
{% include 'helpdesk/faq/faq_nav.html'%}
<div class="oh-wrapper">
{% if faqs %}
<div id="faqList">
{% include "helpdesk/faq/faq_list.html" %}
</div>
{% endif %}
</div>
<div
class="oh-modal"
id="faqCreate"
role="dialog"
aria-labelledby="faqCreate"
aria-hidden="true"
></div>
<div class="oh-modal" id="faqCreate" role="dialog" aria-labelledby="faqCreate" aria-hidden="true"></div>
<div id="addTagTargetModal">
<div id="addTagTarget"></div>
</div>
<div id="addTagTargetModal">
<div id="addTagTarget">
</div>
</div>
<div class="oh-modal" id="addTagModal" role="dialog" aria-labelledby="editDialogModal" aria-hidden="true"
style="z-index: 1100;">
<div class="oh-modal__dialog">
<div class="oh-modal__dialog-header">
<h2 class="oh-modal__dialog-title" id="editTitle">
{% trans "Create Tag" %}
</h2>
<button class="oh-modal__close--custom"
onclick="$(this).parents().closest('.oh-modal--show').toggleClass('oh-modal--show')" aria-label="Close">
<ion-icon name="close-outline"></ion-icon>
</button>
</div>
<div class="oh-modal__dialog-body" id="editTarget">
<form {% comment %} hx-post="{% url 'ticket-create-tag' %}" hx-target="#addTagTarget" method="post"
hx-encoding="multipart/form-data" {% endcomment %} id="addTagForm">
{% csrf_token %}
{{create_tag_f.as_p}}
{% comment %} <button type="submit" class="oh-btn oh-btn--secondary mt-2 mr-0 oh-btn--w-100-resp">
{% trans "Save" %}
</button> {% endcomment %}
</form>
</div>
</div>
</div>
<div
class="oh-modal"
id="addTagModal"
role="dialog"
aria-labelledby="editDialogModal"
aria-hidden="true"
style="z-index: 1100"
>
<div class="oh-modal__dialog">
<div class="oh-modal__dialog-header">
<h2 class="oh-modal__dialog-title" id="editTitle">
{% trans "Create Tag" %}
</h2>
<button
class="oh-modal__close--custom"
onclick="$(this).parents().closest('.oh-modal--show').toggleClass('oh-modal--show')"
aria-label="Close"
>
<ion-icon name="close-outline"></ion-icon>
</button>
</div>
<div class="oh-modal__dialog-body" id="editTarget">
<form id="addTagForm">
{% csrf_token %} {{create_tag_f.as_p}}
</form>
</div>
</div>
</div>
</div>
<script>
function updateTag(event) {
const selectedValues = Array.from(event.selectedOptions).map(option => option.value);
if (selectedValues.includes('create_new_tag')) {
$("#addTagModal").addClass("oh-modal--show");
}
}
function updateTag(event) {
const selectedValues = Array.from(event.selectedOptions).map(
(option) => option.value
);
if (selectedValues.includes("create_new_tag")) {
$("#addTagModal").addClass("oh-modal--show");
}
}
$("#addTagForm").on('submit', function () {
event.preventDefault();
$.ajax({
type: 'POST',
url: '/helpdesk/ticket-create-tag',
data: $(this).serialize(),
success: function (response) {
var newOption = $("<option selected></option>")
.val(response.tag_id)
.text(response.title);
$("#addTagForm").on("submit", function () {
event.preventDefault();
$.ajax({
type: "POST",
url: "/helpdesk/ticket-create-tag",
data: $(this).serialize(),
success: function (response) {
var newOption = $("<option selected></option>")
.val(response.tag_id)
.text(response.title);
$("#id_tags option[value='create_new_tag']").before(newOption);
$("#id_tags option[value='create_new_tag']").prop("selected", false);
$("#addTagModal").removeClass("oh-modal--show");
$('#addTagForm').find('input[name="title"]').val('');
},
})
})
$("#id_tags option[value='create_new_tag']").before(newOption);
$("#id_tags option[value='create_new_tag']").prop(
"selected",
false
);
$("#addTagModal").removeClass("oh-modal--show");
$("#addTagForm").find('input[name="title"]').val("");
},
});
});
</script>
{% endblock %}

View File

@@ -4,7 +4,20 @@
{% include 'helpdesk/faq/faq_category_nav.html'%}
<div class="oh-wrapper">
<div id="faqCategoryList">
{% include "helpdesk/faq/faq_category_list.html" %}
{% if faq_categories %}
{% include "helpdesk/faq/faq_category_list.html" %}
{% else %}
<div class="oh-404">
<img
src="{% static 'images/ui/faq.png' %}"
class="mb-4 oh-404__image"
alt=""
/>
<h3 style="font-size: 20px" class="oh-404__subtitle">
{% trans "There are no FAQs at the moment." %}
</h3>
</div>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,111 @@
{% load static i18n %}
<div class="oh-modal__dialog-header">
<span class="oh-modal__dialog-title">{% trans "Load FAQs" %}</span>
<button class="oh-modal__close" aria-label="Close">
<ion-icon name="close-outline"></ion-icon>
</button>
</div>
<div class="oh-modal__dialog-body">
<div class="oh-card">
<form hx-post="{{request.path}}" hx-target="#objectCreateModalTarget">
<div class="oh-checkpoint-badge text-success mb-2 selectFaqs"
style="cursor: pointer"
>
{% trans "Select All Faqs" %}
</div>
<div class="oh-checkpoint-badge text-secondary mb-2 unSelectFaqs"
style="cursor: pointer"
>
{% trans "Unselect All Faqs" %}
</div>
{% csrf_token %}
{% for cat_id, category in catagories.items %}
<div class="oh-accordion">
<div class="oh-accordion-header">{{category}}</div>
<div
class="oh-accordion-body"
style="
max-height: 500px; overflow-y: auto;
"
>
<span class="d-flex flex-row-reverse mb-2 me-3">
<input
type="checkbox"
title="{% trans 'Select All' %}"
class="custom-radio-checkmark my-2 selectAll"
/>
</span>
<div
class="oh-layout--grid-3 faqs-list"
style="
grid-template-columns: repeat(auto-fill, minmax(48%, 1fr));
"
>
{% for faq in faqs %}
{% if faq.fields.category == cat_id %}
<div class="oh-card rounded">
<div class="oh-kanban-card__details">
<div class="d-flex-justify-between mb-2">
<span class="oh-kanban-card__title"
>{{faq.fields.question}}</span
>
<span>
<input
name="{{faq.pk}}"
type="checkbox"
class="custom-radio-checkmark"
/>
</span>
</div>
<div
class="oh-kanban-card__subtitle truncated-text"
style="
height: 70px;
white-space: wrap;
"
>
{{faq.fields.answer}}
</div>
<ul class="oh-faq__tags m-0 flex-wrap">
{% for tag in faq.tags %}
<li class="oh-faq__tag {% cycle 'bg-primary' 'bg-danger' 'bg-warning' %}">
<span class="oh-faq__tag-text text-light">{{tag}}</span>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endfor %}
<div class="d-flex flex-row-reverse">
<button
type="submit"
class="oh-btn oh-btn--secondary mt-2 mr-0 pl-4 pr-5 oh-btn--w-100-resp"
>
{% trans "Add" %}
</button>
</div>
</form>
</div>
</div>
<script>
$(document).ready(function () {
$(".selectAll").click(function () {
var isChecked = $(this).prop("checked");
$(this).parent().siblings(".faqs-list").find(".custom-radio-checkmark").prop("checked", isChecked);
});
$(".selectFaqs").click(function () {
$(".selectAll").prop("checked", false);
$(".selectAll").click();
});
$(".unSelectFaqs").click(function () {
$(".selectAll").prop("checked", true);
$(".selectAll").click();
});
});
</script>

View File

@@ -115,7 +115,7 @@
</div>
<div class = "oh-modal__button-container text-center mt-3" onclick="event.stopPropagation()">
<div class="oh-btn-group" style="border:none">
{% if ticket|calim_request_exists:request.user.employee_get or request.user.employee_get in ticket.assigned_to.all %}
{% if ticket|claim_request_exists:request.user.employee_get or request.user.employee_get in ticket.assigned_to.all %}
<a
href="#"
class="oh-btn oh-btn--info w-100 oh-btn--disabled"

View File

@@ -6,8 +6,8 @@ from helpdesk.models import ClaimRequest, DepartmentManager
register = template.Library()
@register.filter(name="calim_request_exists")
def calim_request_exists(ticket, employee):
@register.filter(name="claim_request_exists")
def claim_request_exists(ticket, employee):
return ClaimRequest.objects.filter(ticket_id=ticket, employee_id=employee).exists()

View File

@@ -153,4 +153,5 @@ urlpatterns = [
views.delete_ticket_document,
name="delete-ticket-document",
),
path("load-faqs/", views.load_faqs, name="load-faqs"),
]

View File

@@ -6,7 +6,9 @@ from distutils.util import strtobool
from operator import itemgetter
from urllib.parse import parse_qs
from django.conf import settings
from django.contrib import messages
from django.core import serializers
from django.db.models import ProtectedError
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import redirect, render
@@ -203,6 +205,10 @@ def faq_view(request, obj_id, **kwargs):
"""
faqs = FAQ.objects.filter(category=obj_id)
faq_category = FAQCategory.objects.filter(id=obj_id)
if not faq_category:
messages.info(request, _("No FAQ found for the given category."))
return redirect(faq_category_view)
context = {
"faqs": faqs,
"f": FAQFilter(request.GET),
@@ -1691,3 +1697,116 @@ def get_department_employees(request):
context = {"employees": employees}
employee_html = render_to_string("employee/employees_select.html", context)
return HttpResponse(employee_html)
@login_required
def load_faqs(request):
base_dir = settings.BASE_DIR
faq_file = os.path.join(base_dir, "load_data", "faq.json")
faq_category_file = os.path.join(base_dir, "load_data", "faq_category.json")
tags_file = os.path.join(base_dir, "load_data", "tags.json")
with open(faq_category_file, "r") as cats:
faq_category_raw = json.load(cats)
with open(tags_file, "r") as t:
tags_raw = json.load(t)
with open(faq_file, "r") as faqs:
faq_raw = json.load(faqs)
category_lookup = {item["pk"]: item["fields"]["title"] for item in faq_category_raw}
tag_lookup = {item["pk"]: item["fields"]["title"] for item in tags_raw}
if request.method == "POST":
selected_ids = [int(k) for k in request.POST.keys() if k.isdigit()]
selected_faqs = [a for a in faq_raw if a["pk"] in selected_ids]
category_needed = [
faq["fields"].get("category")
for faq in selected_faqs
if faq["fields"].get("category")
]
tags_needed_list = [
faq["fields"].get("tags")
for faq in selected_faqs
if faq["fields"].get("tags")
]
tags_needed = [item for item_list in tags_needed_list for item in item_list]
for category_json in faq_category_raw:
if category_json["pk"] in category_needed:
category_data = list(
serializers.deserialize("json", json.dumps([category_json]))
)[0].object
existing = FAQCategory.objects.filter(title=category_data.title).first()
if not existing:
category_data.pk = None
category_data.save()
for tag_json in tags_raw:
if tag_json["pk"] in tags_needed:
tag_data = list(
serializers.deserialize("json", json.dumps([tag_json]))
)[0].object
existing = Tags.objects.filter(title=tag_data.title).first()
if not existing:
tag_data.pk = None
tag_data.save()
for faq_json in selected_faqs:
deserialized = list(
serializers.deserialize("json", json.dumps([faq_json]))
)[0]
faq_obj = deserialized.object
category_pk = faq_json["fields"].get("category")
tag_pk = faq_json["fields"].get("tags")
category_title = category_lookup.get(category_pk)
tags_title = [tag_lookup.get(pk, "") for pk in tag_pk]
category = FAQCategory.objects.filter(title=category_title).first()
tags = Tags.objects.filter(title__in=tags_title)
faq_obj.category = category
if not FAQ.objects.filter(question=faq_obj.question).exists():
faq_obj.pk = None
faq_obj.save()
faq_obj.tags.set(tags)
messages.success(
request, f"Automation '{faq_obj.question}' created successfully."
)
else:
messages.warning(
request, f"Automation '{faq_obj.question}' already exists."
)
script = """
<script>
$('.oh-modal--show').removeClass('oh-modal--show');
$('#reloadMessagesButton').click();
$('.filterButton').click();
</script>
"""
return HttpResponse(script)
processed_faqs = []
for faq in faq_raw:
processed = faq.copy()
category_pk = faq["fields"].get("category")
tag_pk = faq["fields"].get("tags")
processed["tags"] = [tag_lookup.get(pk, "") for pk in tag_pk]
processed["category"] = category_lookup.get(category_pk, "")
processed_faqs.append(processed)
return render(
request,
"helpdesk/faq/load_faq.html",
{
"faqs": processed_faqs,
"catagories": category_lookup,
},
)