Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export page access statistics #3394

Open
wants to merge 61 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
39de968
Merge page_tree_archived into page_tree
jarlhengstmengel Nov 27, 2024
63219f4
Fix translation format
JoeyStk Nov 27, 2024
8eb7279
Add template for statistics about viewed pages
JoeyStk Nov 27, 2024
94fcc30
Add _generic_page_tree.html
JoeyStk Nov 27, 2024
3c12cc7
Add additional column to page tree on statistics
JoeyStk Nov 27, 2024
b1545cc
Fix missing backend titles for root pages in page tree and deleting t…
jarlhengstmengel Nov 29, 2024
9edae89
Merging page_tree_node and page_tree_archived_node
jarlhengstmengel Dec 2, 2024
e3c0d63
Disable dragable tree node for archived tree nodes
jarlhengstmengel Dec 3, 2024
8466b66
[WIP] Page based access
JoeyStk Dec 6, 2024
813b751
[WIP] Restored commit
JoeyStk Dec 7, 2024
04ce650
Fix loading of childnodes
jarlhengstmengel Dec 9, 2024
aed75ed
Fix codestyle
jarlhengstmengel Dec 9, 2024
05c2ad1
Add _generic_page_tree_node.hmtl
jarlhengstmengel Dec 9, 2024
7128904
update translations
jarlhengstmengel Dec 9, 2024
718358e
Cleaning up and deleting page_tree_archived files
jarlhengstmengel Dec 9, 2024
4fd9720
Rename page_tree to pages_page_tree
jarlhengstmengel Dec 9, 2024
feb7a64
Add page accesses calls
JoeyStk Dec 9, 2024
a41035d
Merge branch 'feature/refactor_page_tree_and_add_statistics_to_collap…
JoeyStk Dec 9, 2024
bbfb58c
Fix pylint
JoeyStk Dec 9, 2024
c375fde
[WIP] Include page tree to statistics
JoeyStk Dec 10, 2024
68ccfe1
Javascript code
JoeyStk Dec 10, 2024
a9fa6ae
[WIP] First idea for graph
JoeyStk Dec 12, 2024
edc3114
Show bars
JoeyStk Dec 12, 2024
0308511
Merge page_tree_archived into page_tree
jarlhengstmengel Nov 27, 2024
562f952
Fix translation format
JoeyStk Nov 27, 2024
836506b
Add template for statistics about viewed pages
JoeyStk Nov 27, 2024
bc93bc6
Add _generic_page_tree.html
JoeyStk Nov 27, 2024
ce301cc
Add additional column to page tree on statistics
JoeyStk Nov 27, 2024
4f4c99d
Fix missing backend titles for root pages in page tree and deleting t…
jarlhengstmengel Nov 29, 2024
4f91410
Merging page_tree_node and page_tree_archived_node
jarlhengstmengel Dec 2, 2024
046c9d0
Disable dragable tree node for archived tree nodes
jarlhengstmengel Dec 3, 2024
a022d8f
[WIP] Page based access
JoeyStk Dec 6, 2024
ef21cf8
[WIP] Restored commit
JoeyStk Dec 7, 2024
4801e17
Add page accesses calls
JoeyStk Dec 9, 2024
b882d07
Fix loading of childnodes
jarlhengstmengel Dec 9, 2024
5b66426
Fix codestyle
jarlhengstmengel Dec 9, 2024
160d621
Add _generic_page_tree_node.hmtl
jarlhengstmengel Dec 9, 2024
89fd0fd
update translations
jarlhengstmengel Dec 9, 2024
962f0c5
Cleaning up and deleting page_tree_archived files
jarlhengstmengel Dec 9, 2024
5df5e37
Rename page_tree to pages_page_tree
jarlhengstmengel Dec 9, 2024
495e71b
Fix pylint
JoeyStk Dec 9, 2024
7a82550
[WIP] Include page tree to statistics
JoeyStk Dec 10, 2024
71c19ab
Javascript code
JoeyStk Dec 10, 2024
b0ffcc6
[WIP] First idea for graph
JoeyStk Dec 12, 2024
c502c28
Show bars
JoeyStk Dec 12, 2024
b1da1bc
Add option to change time range and language selection
JoeyStk Dec 28, 2024
424448f
Disable editable page tree in statistics
jarlhengstmengel Jan 7, 2025
9305396
Upgrade lucide dependicy
jarlhengstmengel Jan 7, 2025
b183cc5
Update translations
jarlhengstmengel Jan 8, 2025
4d0ff3e
Merge branch 'feature/refactor_page_tree_and_add_statistics_to_collap…
jarlhengstmengel Jan 8, 2025
29809d1
Fix page tree
jarlhengstmengel Jan 8, 2025
4990aea
Fix archived page tree
jarlhengstmengel Jan 11, 2025
7af0938
Fix code styles
JoeyStk Jan 11, 2025
120f93c
Update lucide version
JoeyStk Jan 13, 2025
1864936
Fix existing tests
jarlhengstmengel Jan 20, 2025
1223224
Fix format
jarlhengstmengel Jan 20, 2025
9e6374b
Add percentage to accesses
JoeyStk Jan 24, 2025
70f7f62
Add comments to statistics-page-accesses
JoeyStk Jan 24, 2025
d7842be
Apply suggestions from code review
JoeyStk Jan 28, 2025
7d82374
Update statistics_actions and utils
JoeyStk Jan 28, 2025
f92a15f
Add dropdown for statistics selection
JoeyStk Dec 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ module.exports = {
"implicit-arrow-linebreak": "off",
"max-len": "off",
"no-confusing-arrow": "off",
"no-console": "off",
"no-multiple-empty-lines": "off",
"object-curly-newline": "off",
"operator-linebreak": "off",
Expand Down Expand Up @@ -137,6 +136,7 @@ module.exports = {
"react/function-component-definition": ["error", { namedComponents: "arrow-function" }],
"react-hooks/exhaustive-deps": "error",
"vars-on-top": "error",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
},
overrides: [
{
Expand Down
125 changes: 78 additions & 47 deletions integreat_cms/api/v3/chat/utils/chat_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,96 +4,124 @@

from __future__ import annotations

import requests
import asyncio

import aiohttp
from celery import shared_task
from django.conf import settings

from integreat_cms.cms.models import Region, UserChat
from integreat_cms.cms.utils.content_translation_utils import (
get_public_translation_for_link,
)

from .zammad_api import ZammadChatAPI


def format_message(response: dict) -> str:
async def automatic_answer(
message: str, region_slug: str, language_slug: str, session: aiohttp.ClientSession
) -> dict:
"""
Transform JSON into readable message
"""
if "answer" not in response or not response["answer"]:
raise ValueError("Could not format message, no answer attribute in response")
if "sources" not in response or not response["sources"]:
return response["answer"]
sources = "".join(
[
f"<li><a href='{settings.WEBAPP_URL}{path}'>{title}</a></li>"
for path in response["sources"]
if (title := get_public_translation_for_link(settings.WEBAPP_URL + path))
]
)
return f"{response['answer']}\n<ul>{sources}</ul>"


def automatic_answer(message: str, region: Region, language_slug: str) -> str | None:
"""
Get automatic answer to question
Get automatic answer to question asynchronously
"""
url = (
f"https://{settings.INTEGREAT_CHAT_BACK_END_DOMAIN}/chatanswers/extract_answer/"
)
body = {"message": message, "language": language_slug, "region": region.slug}
r = requests.post(url, json=body, timeout=settings.INTEGREAT_CHAT_BACK_END_TIMEOUT)
return format_message(r.json())
body = {"message": message, "language": language_slug, "region": region_slug}
async with session.post(
url, json=body, timeout=settings.INTEGREAT_CHAT_BACK_END_TIMEOUT
) as response:
return await response.json()


def automatic_translation(
message: str, source_language_slug: str, target_language_slug: str
) -> str:
async def automatic_translation(
message: str,
source_language_slug: str,
target_language_slug: str,
session: aiohttp.ClientSession,
) -> dict:
"""
Use LLM to translate message
Use LLM to translate message asynchronously
"""
url = f"https://{settings.INTEGREAT_CHAT_BACK_END_DOMAIN}/chatanswers/translate_message/"
body = {
"message": message,
"source_language": source_language_slug,
"target_language": target_language_slug,
}
response = requests.post(
async with session.post(
url, json=body, timeout=settings.INTEGREAT_CHAT_BACK_END_TIMEOUT
).json()
if "status" in response and response["status"] == "success":
return response["translation"]
raise ValueError("Did not receive success response for translation request.")
) as response:
return await response.json()


async def async_process_user_message(
zammad_chat_language_slug: str,
region_slug: str,
region_default_language_slug: str,
message_text: str,
) -> tuple[dict, dict]:
"""
Process the message from an Integreat App user
"""
async with aiohttp.ClientSession() as session:
translation_task = automatic_translation(
message_text,
zammad_chat_language_slug,
region_default_language_slug,
session,
)
answer_task = automatic_answer(
message_text, region_slug, zammad_chat_language_slug, session
)
translation, answer = await asyncio.gather(translation_task, answer_task)
return translation, answer


@shared_task
def process_user_message(
message_text: str, region_slug: str, zammad_ticket_id: int
) -> None:
"""
Process the message from an Integreat App user
Call the async processing of the message from an Integreat App user
"""
zammad_chat = UserChat.objects.get(zammad_id=zammad_ticket_id)
region = Region.objects.get(slug=region_slug)
client = ZammadChatAPI(region)
if translation := automatic_translation(
message_text, zammad_chat.language.slug, region.default_language.slug
):
translation, answer = asyncio.run(
async_process_user_message(
zammad_chat.language.slug,
region_slug,
region.default_language.slug,
message_text,
)
)
if translation:
client.send_message(
zammad_chat.zammad_id,
translation,
translation["translation"],
True,
True,
)
if answer := automatic_answer(message_text, region, zammad_chat.language.slug):
if answer:
client.send_message(
zammad_chat.zammad_id,
answer,
answer["answer"],
False,
True,
)


async def async_process_answer(
message_text: str, source_language: str, target_language: str
) -> dict:
"""
Process automatic or counselor answers
"""
async with aiohttp.ClientSession() as session:
translation_task = automatic_translation(
message_text, source_language, target_language, session
)
return await translation_task


@shared_task
def process_answer(message_text: str, region_slug: str, zammad_ticket_id: int) -> None:
"""
Expand All @@ -102,12 +130,15 @@ def process_answer(message_text: str, region_slug: str, zammad_ticket_id: int) -
zammad_chat = UserChat.objects.get(zammad_id=zammad_ticket_id)
region = Region.objects.get(slug=region_slug)
client = ZammadChatAPI(region)
if translation := automatic_translation(
message_text, region.default_language.slug, zammad_chat.language.slug
):
translation = asyncio.run(
async_process_answer(
message_text, region.default_language.slug, zammad_chat.language.slug
)
)
if translation:
client.send_message(
zammad_chat.zammad_id,
translation,
translation["translation"],
False,
True,
)
7 changes: 5 additions & 2 deletions integreat_cms/api/v3/chat/utils/zammad_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def _transform_attachment(
)[0].random_hash,
}

def get_messages(self, chat: UserChat) -> dict[str, dict | list[dict]]:
def get_messages(self, chat: UserChat) -> dict[str, dict | list[dict] | str]:
# pylint: disable=method-hidden
"""
Get all non-internal messages for a given ticket
Expand All @@ -163,7 +163,10 @@ def get_messages(self, chat: UserChat) -> dict[str, dict | list[dict]]:
for attachment in message["attachments"]
]

return {"messages": response}
return {
"messages": response,
"ticket_url": f"{chat.region.zammad_url}/#ticket/zoom/{chat.zammad_id}",
}

def send_message(
self,
Expand Down
30 changes: 30 additions & 0 deletions integreat_cms/cms/constants/calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
This module contains all string representations of calendar filter options, used by :class:`~integreat_cms.cms.forms.events.event_filter_form.EventFilterForm` and
:class:`~integreat_cms.cms.views.events.event_list_view.EventListView`:

The module also contains a constant :const:`~integreat_cms.cms.constants.calendar.DATATYPE` which contains the type of the constant values linked to the strings and is used for correctly
instantiating :class:`django.forms.TypedMultipleChoiceField` instances in :class:`~integreat_cms.cms.forms.events.event_filter_form.EventFilterForm`.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from django.utils.translation import gettext_lazy as _

if TYPE_CHECKING:
from typing import Final

from django.utils.functional import Promise

DATATYPE: Final = int

EVENT_NOT_FROM_EXTERNAL_CALENDAR: Final = 1
#: Events that are not recurring, i.e. take place only once
EVENT_FROM_EXTERNAL_CALENDAR: Final = 2

#: Choices to use these constants in a database field
CHOICES: Final[list[tuple[int, Promise]]] = [
(EVENT_NOT_FROM_EXTERNAL_CALENDAR, _("Event not from an external calendar")),
(EVENT_FROM_EXTERNAL_CALENDAR, _("Event from an external calendar")),
]
Loading