[UPDT] HELPDESK: Updated faq search method and removed haystack search

This commit is contained in:
Horilla
2025-04-28 14:32:13 +05:30
parent 5b2e45eb59
commit da165b1aab
12 changed files with 230 additions and 353 deletions

View File

@@ -109,3 +109,21 @@ class TicketReGroup:
("assigned_to", "Assigner"),
("employee_id__employee_work_info__company_id", "Company"),
]
class FaqSearch(FilterSet):
search = CharFilter(method="search_method", lookup_expr="icontains")
class Meta:
model = FAQ
fields = ["search"]
def search_method(self, queryset, _, value):
"""
This method is used to add custom search condition
"""
return (
queryset.filter(question__icontains=value)
| queryset.filter(answer__icontains=value)
| queryset.filter(tags__title__icontains=value)
).distinct()

View File

@@ -275,24 +275,3 @@ class FAQ(HorillaModel):
class Meta:
verbose_name = _("FAQ")
verbose_name_plural = _("FAQs")
# updating the faq search index when a new faq is created or deleted
def update_index(sender, instance, **kwargs):
from .search_indexes import FAQIndex
index = FAQIndex()
index.update_object(instance)
def remove_from_index(sender, instance, **kwargs):
from .search_indexes import FAQIndex
index = FAQIndex()
index.remove_object(instance)
post_save.connect(update_index, sender=FAQ)
post_delete.connect(remove_from_index, sender=FAQ)

View File

@@ -1,12 +0,0 @@
from haystack import indexes
from .models import FAQ
class FAQIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
question = indexes.CharField(model_attr="question")
answer = indexes.CharField(model_attr="answer")
def get_model(self):
return FAQ

View File

@@ -1,49 +1,102 @@
{% load i18n %}
{% load i18n static %}
{% include 'filter_tags.html' %}
<div class="oh-faq-cards">
{% for category in faq_categories %}
<div class="oh-faq-card" id="faqCategoryItem{{category.id}}">
<div class="d-flex justify-content-between align-items-center">
<h3 class="oh-faq-card__title">{{category.title}}</h3>
{% if perms.helpdesk.change_faqcategory or perms.helpdesk.delete_faqcategory %}
<div class="oh-dropdown" x-data="{open: false}">
<button class="oh-btn oh-stop-prop oh-btn--transparent oh-accordion-meta__btn" @click="open = !open"
@click.outside="open = false" title="Actions">
<ion-icon name="ellipsis-vertical" role="img" class="md hydrated"
aria-label="ellipsis vertical"></ion-icon>
</button>
<div class="oh-faq-card" id="faqCategoryItem{{category.id}}">
<div class="d-flex justify-content-between align-items-center">
<h3 class="oh-faq-card__title">{{category.title}}</h3>
{% if perms.helpdesk.change_faqcategory or perms.helpdesk.delete_faqcategory %}
<div class="oh-dropdown" x-data="{open: false}">
<button
class="oh-btn oh-stop-prop oh-btn--transparent oh-accordion-meta__btn"
@click="open = !open"
@click.outside="open = false"
title="Actions"
>
<ion-icon
name="ellipsis-vertical"
role="img"
class="md hydrated"
aria-label="ellipsis vertical"
></ion-icon>
</button>
<div class="oh-dropdown__menu oh-dropdown__menu--right" x-show="open">
<ul class="oh-dropdown__items">
{% if perms.helpdesk.change_faqcategory %}
<li class="oh-dropdown__item">
<a hx-get="{% url 'faq-category-update' category.id %}" hx-target="#objectCreateModalTarget"
data-toggle="oh-modal-toggle" data-target="#objectCreateModal" role="button"
class="oh-dropdown__link">{% trans "Edit" %}</a>
</li>
{% endif %}
{% if perms.helpdesk.delete_faqcategory %}
<li class="oh-dropdown__item">
<form hx-confirm="{% trans 'Are you sure you want to delete this FAQ Category?' %}"
hx-post="{% url 'faq-category-delete' category.id %}" hx-swap="outerHTML"
hx-on-htmx-after-request="setTimeout(() => {reloadMessage(this);},100);"
hx-target="#faqCategoryItem{{category.id}}">
{% csrf_token %}
<button type="submit" class="oh-dropdown__link oh-dropdown__link--danger">
{% trans "Delete" %}
</button>
</form>
</li>
{% endif %}
</ul>
</div>
</div>
{% endif %}
<div
class="oh-dropdown__menu oh-dropdown__menu--right"
x-show="open"
>
<ul class="oh-dropdown__items">
{% if perms.helpdesk.change_faqcategory %}
<li class="oh-dropdown__item">
<a
hx-get="{% url 'faq-category-update' category.id %}"
hx-target="#objectCreateModalTarget"
data-toggle="oh-modal-toggle"
data-target="#objectCreateModal"
role="button"
class="oh-dropdown__link"
>{% trans "Edit" %}</a
>
</li>
{% endif %}
{% if perms.helpdesk.delete_faqcategory %}
<li class="oh-dropdown__item">
<form
hx-confirm="{% trans 'Are you sure you want to delete this FAQ Category?' %}"
hx-post="{% url 'faq-category-delete' category.id %}"
hx-swap="outerHTML"
hx-on-htmx-after-request="setTimeout(() => {reloadMessage(this);},100);"
hx-target="#faqCategoryItem{{category.id}}"
>
{% csrf_token %}
<button
type="submit"
class="oh-dropdown__link oh-dropdown__link--danger"
>
{% trans "Delete" %}
</button>
</form>
</li>
{% endif %}
</ul>
</div>
</div>
<p class="oh-faq-card__desc">{{category.description}}</p>
<a href="{% url 'faq-view' category.id %}" class="oh-btn oh-btn--secondary oh-btn--block">{% trans "View FAQs" %}</a>
{% endif %}
</div>
<p class="oh-faq-card__desc">{{category.description}}</p>
<a
href="{% url 'faq-view' category.id %}"
class="oh-btn oh-btn--secondary oh-btn--block"
>{% trans "View FAQs" %}</a
>
</div>
{% 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 %}
</div>

View File

@@ -2,7 +2,7 @@
<section class="oh-wrapper oh-main__topbar" x-data="{searchShow: false}">
<div class="oh-main__titlebar oh-main__titlebar--left">
<h1 class="oh-main__titlebar-title fw-bold">
{{model.get_verbose_name_plural}}
{% trans "FAQ Categories" %}
</h1>
<a
class="oh-main__titlebar-search-toggle"
@@ -36,12 +36,9 @@
name="search"
placeholder="Search in FAQs..."
hx-get="{% url 'faq-search' %}?category=true"
hx-trigger="change delay:300ms"
onkeyup='$("#suggestion_box").show(); if ($(this).val() == "") {$("#suggestion_box").hide()}'
onfocusout = '$("#suggestion_box").fadeOut(500)'
hx-trigger="change"
hx-target="#faqCategoryList"
/>
<div class="oh-autocomplete-suggestions" id="suggestion_box"></div>
</div>
{% comment %}
<select class="oh-select oh-select-faq oh-select--dropdown">

View File

@@ -1,4 +1,4 @@
{% load i18n %}
{% load i18n static %}
{% include 'filter_tags.html' %}
<div class="oh-card mb-4">
<div class="oh-faq">
@@ -40,6 +40,22 @@
</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>

View File

@@ -1,99 +1,44 @@
{% extends 'index.html' %} {% block content %} {% load static %} {% load i18n %}
{% include 'helpdesk/faq/faq_nav.html'%}
<style>
.oh-faq__item-body {
background: #e9dfec9c;
opacity: 0.7;
}
<div id="faqContainer">
.oh-faq__tag {
border-radius: 10px;
font-weight: 600;
}
.oh-faq__item-body {
max-height: 0;
transition: max-height 0.3s ease, padding 0.3s ease;
overflow-y: auto;
}
.oh-faq__item--show .oh-faq__item-body {
max-height: 200px;
/* Adjust the max-height as needed */
}
.oh-title_faq__main-header {
width: 70%;
}
.oh-select-faq {
width: 12%;
}
.oh-select-faq,
.oh-select-faq:last-child,
.oh-faq__input-search {
margin: 1rem 0;
}
.oh-faq_search--icon {
top: 17px;
}
</style>
<div class="oh-wrapper">
{% if faqs %}
<div id="faqList">
{% include "helpdesk/faq/faq_list.html" %}
{% include 'helpdesk/faq/faq_nav.html'%}
<div class="oh-wrapper">
{% if faqs %}
<div id="faqList">
{% include "helpdesk/faq/faq_list.html" %}
</div>
{% endif %}
</div>
{% else %}
<div style="
height: 70vh;
display: flex;
align-items: center;
justify-content: center;
">
<div class="oh-404">
<img style="display: block; width: 150px; height: 150px; margin: 10px auto"
src="{% static 'images/ui/attendance.png' %}" class="mb-4" alt="" />
<h3 style="font-size: 20px" class="oh-404__subtitle">
{% trans "There are no FAQs at the moment." %}
</h3>
<div class="oh-modal" id="faqCreate" role="dialog" aria-labelledby="faqCreate" aria-hidden="true"></div>
<div id="addTagTargetModal">
<div id="addTagTarget">
</div>
</div>
{% endif %}
</div>
<!-- create FAQ modal -->
<div class="oh-modal" id="faqCreate" role="dialog" aria-labelledby="faqCreate" aria-hidden="true"></div>
<!-- modal -->
<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 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>
@@ -106,15 +51,6 @@
}
}
function show_answer(element) {
if ($(element).parent(".oh-faq__item--show").length != 0) {
$(".oh-faq__item--show").removeClass("oh-faq__item--show");
} else {
$(".oh-faq__item--show").removeClass("oh-faq__item--show");
$(element).parent('.oh-faq__item').addClass("oh-faq__item--show");
}
}
$("#addTagForm").on('submit', function () {
event.preventDefault();
$.ajax({

View File

@@ -1,143 +1,62 @@
{% extends 'index.html' %} {% block content %} {% load static %} {% load i18n %}
{% include 'helpdesk/faq/faq_category_nav.html'%}
<style>
.oh-faq__item-body {
background: #e9dfec9c;
opacity: 0.7;
}
.oh-faq__tag {
border-radius: 10px;
font-weight: 600;
}
.oh-faq__item-body {
max-height: 0;
transition: max-height 0.3s ease,padding 0.3s ease;
overflow-y: auto;
}
.oh-faq__item--show .oh-faq__item-body {
max-height: 200px; /* Adjust the max-height as needed */
}
.oh-title_faq__main-header {
width:70%;
}
.oh-select-faq {
width:12%;
}
.oh-select-faq , .oh-select-faq:last-child , .oh-faq__input-search {
margin :1rem 0;
}
<div id="faqContainer">
{% include 'helpdesk/faq/faq_category_nav.html'%}
<div class="oh-wrapper">
<div id="faqCategoryList">
{% include "helpdesk/faq/faq_category_list.html" %}
</div>
</div>
h1.oh-main__titlebar-title{
margin-bottom:0;
}
.oh-faq_search--icon{
top:17px;
}
</style>
<div class="oh-wrapper">
{% if faq_categories %}
<div id="faqCategoryList">
{% include "helpdesk/faq/faq_category_list.html" %}
</div>
{% else %}
<div
style="
height: 70vh;
display: flex;
align-items: center;
justify-content: center;
"
class=""
>
<div style="" 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>
{% endif %}
<!-- Sticky Table -->
<div
class="oh-modal"
id="faqCategoryCreate"
role="dialog"
aria-labelledby="faqCategoryCreate"
aria-hidden="true"
></div>
<div
class="oh-modal"
id="faqCreate"
role="dialog"
aria-labelledby="faqCreate"
aria-hidden="true"
></div>
</div>
<!-- create FAQ modal -->
<div
class="oh-modal"
id="faqCategoryCreate"
role="dialog"
aria-labelledby="faqCategoryCreate"
aria-hidden="true"
></div>
<!-- modal -->
<!-- create FAQ modal -->
<div
class="oh-modal"
id="faqCreate"
role="dialog"
aria-labelledby="faqCreate"
aria-hidden="true"
></div>
<!-- modal -->
<script>
function completeSuggestion (element) {
$('.oh-search_input').val($(element).text());
$("#suggestion_box").hide();
}
var get_faqs = () => {
var FAQList = {{ questions|safe }};
$(function () {
$("[name='search']").autocomplete({
source: FAQList,
appendTo:".oh-faq__input-search",
select: function (event, ui) {
$(this).val(ui.item.value);
$(this).blur();
},
open: function () {
$(".ui-autocomplete").addClass("oh-autocomplete-suggestions");
$(".ui-autocomplete li").addClass("autocomplete-suggestion");
}
});
});
};
function show_answer(element){
if ($(element).parent(".oh-faq__item--show").length !=0){
$(".oh-faq__item--show").removeClass("oh-faq__item--show");
} else {
$(".oh-faq__item--show").removeClass("oh-faq__item--show");
$(element).parent('.oh-faq__item').addClass("oh-faq__item--show");
}
}
$(document).ready(function () {
get_faqs();
let typingTimer;
var typingDelay = 1000;
$(document).ready(function () {
$("[name='search']").on("input", function () {
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
this.dispatchEvent(new Event('change', { bubbles: true }));
}, typingDelay);
});
});
var autocompleteSuggestions = $('.oh-autocomplete-suggestions');
var searchInput = $('.oh-search_input');
searchInput.on('input', function(){
let search = searchInput.val()
$.ajax({
type: "get",
url: `/helpdesk/faq-suggestion/`,
data: {
csrfmiddlewaretoken: getCookie("csrftoken"),
"search": search,
},
success: function (response) {
$("#suggestion_box").html("")
$.each(response.faqs, function(index,faq){
$("#suggestion_box").append(
`<div class="autocomplete-suggestion" onclick=completeSuggestion(this)>${faq.question}</div>`
)
}
)
},
error: function(){
console.log("error")
}
})
})
$(document).on('click', function (event) {
if (!searchInput.is(event.target) && !autocompleteSuggestions.is(event.target) && autocompleteSuggestions.has(event.target).length === 0) {
$("#suggestion_box").html('');
}
});
});
$(document).ready(function(){
});
$(document).on("htmx:afterRequest", function (e) {
get_faqs();
});
</script>
{% endblock %}

View File

@@ -1,2 +0,0 @@
{{ object.title }}
{{ object.content }}

View File

@@ -7,15 +7,13 @@ from operator import itemgetter
from urllib.parse import parse_qs
from django.contrib import messages
from django.core.paginator import Paginator
from django.db.models import ProtectedError, Q
from django.db.models import ProtectedError
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import redirect, render
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.translation import gettext as _
from django.views.decorators.http import require_http_methods
from haystack.query import SearchQuerySet
from base.forms import TagsForm
from base.methods import (
@@ -29,7 +27,13 @@ from base.models import Department, JobPosition, Tags
from employee.models import Employee
from employee.views import get_content_type
from helpdesk.decorators import ticket_owner_can_enter
from helpdesk.filter import FAQCategoryFilter, FAQFilter, TicketFilter, TicketReGroup
from helpdesk.filter import (
FAQCategoryFilter,
FAQFilter,
FaqSearch,
TicketFilter,
TicketReGroup,
)
from helpdesk.forms import (
AttachmentForm,
CommentForm,
@@ -79,10 +83,10 @@ def faq_category_view(request):
"""
faq_categories = FAQCategory.objects.all()
questions = FAQ.objects.values_list("question", flat=True)
context = {
"faq_categories": faq_categories,
"f": FAQFilter(request.GET),
"model": FAQCategory,
"questions": list(questions),
}
return render(request, "helpdesk/faq/faq_view.html", context=context)
@@ -286,18 +290,11 @@ def faq_search(request):
category = request.GET.get("category", "")
previous_data = request.GET.urlencode()
query = request.GET.get("search", "")
faqs = FAQ.objects.filter(is_active=True)
data_dict = parse_qs(previous_data)
get_key_instances(FAQ, data_dict)
if query:
results_list = (
SearchQuerySet()
.filter(Q(question__icontains=query) | Q(answer__icontains=query))
.using("default")
)
result_pks = [result.pk for result in results_list]
faqs = FAQ.objects.filter(pk__in=result_pks)
faqs = FaqSearch(request.GET).qs
else:
faqs = FAQ.objects.filter(is_active=True)
@@ -309,6 +306,7 @@ def faq_search(request):
faqs = faqs.filter(category=id)
if category:
data_dict.pop("category")
context = {
"faqs": faqs,
"f": FAQFilter(request.GET),

View File

@@ -3,7 +3,6 @@ init.py
"""
from horilla import (
haystack_configuration,
horilla_apps,
horilla_context_processors,
horilla_middlewares,

View File

@@ -1,24 +0,0 @@
import os
from horilla import settings
setattr(
settings,
"HAYSTACK_CONNECTIONS",
{
"default": {
"ENGINE": "haystack.backends.whoosh_backend.WhooshEngine",
"PATH": os.path.join(
settings.BASE_DIR, "whoosh_index"
), # Set the path to the Whoosh index
},
},
)
setattr(
settings,
"HAYSTACK_SIGNAL_PROCESSORS",
{
"helpdesk": "helpdesk.signals.SignalProcessor",
},
)