Skip to content

Commit

Permalink
Feature/create interaction log (#5)
Browse files Browse the repository at this point in the history
* Add view to create and download eventlogs

* Add download button to sidebar to download data as spreadsheet

---------

Co-authored-by: David Mang <[email protected]>
  • Loading branch information
mangdavid and David Mang authored Sep 1, 2024
1 parent 9dff50f commit 0b2c039
Show file tree
Hide file tree
Showing 25 changed files with 425 additions and 8 deletions.
23 changes: 23 additions & 0 deletions client/src/api/interaction.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { EventLogBody } from "../types/interaction/interaction.types";
import api from "./interceptor.api";

export const createEventLog = async (eventlog: EventLogBody) => {
try {
const response = await api.post("/api/v1/event-logs/", eventlog);
return response;
} catch (error) {
console.error("Error creating event log:", error);
throw error;
}
};

export const fetchEventLogs = async () => {
try {
const response = await api.get(`/api/v1/event-logs/`, {
responseType: "blob",
});
return response;
} catch (error) {
throw error;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Button, Typography, useTheme } from "@mui/material";
import { getEventLogs } from "../../../services/interactions.service";
import { Download } from "react-feather";

const DownloadButton = () => {
const theme = useTheme();

const handleDownload = async () => {
try {
const response = await getEventLogs();

// Create a link element, set its href to the blob URL, and trigger a click to download the file
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "user_interaction_data.xlsx"); // The file name you want to save as
document.body.appendChild(link);
link.click();

// Clean up and remove the link
link.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error("Error downloading file:", error);
}
};

return (
<Button
variant="contained"
startIcon={<Download />}
sx={{ background: theme.palette.primary.dark, textTransform: "none" }}
onClick={handleDownload}
>
<Typography variant="body2">Download Data</Typography>
</Button>
);
};

export default DownloadButton;
26 changes: 26 additions & 0 deletions client/src/components/sidebar/sidebar.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
import { useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { getUserPermissions } from "../../services/user.service";
import { addEventLog } from "../../services/interactions.service";
import DownloadButton from "./download-button/download-button.component";

function Sidebar({ open, setOpen }: SidebarParams) {
const navigate = useNavigate();
Expand Down Expand Up @@ -112,6 +114,17 @@ function Sidebar({ open, setOpen }: SidebarParams) {
}}
onClick={() => {
navigate(item.link);
if (
(import.meta.env.VITE_ENABLE_TRACKING as string) == "true"
) {
if (item.name == "Insights") {
addEventLog({
location: item.name + " - Behavioral Indicators",
});
} else {
addEventLog({ location: item.name });
}
}
}}
>
<Stack
Expand Down Expand Up @@ -142,6 +155,19 @@ function Sidebar({ open, setOpen }: SidebarParams) {
</Grid>
);
})}
<Grid
item
xs={12}
sx={{
display: "flex",
justifyContent: "flex-start",
ml: "1rem",
mr: "1rem",
mt: "3vh",
}}
>
<DownloadButton />
</Grid>
</Grid>
<Grid
container
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Grid, Paper, Stack, Typography, useTheme } from "@mui/material";
import { SingleBarGraphParams } from "../../../../types/statistics/statistics.types";
import ReactWordcloud from "react-wordcloud";
import { ResponsiveContainer } from "recharts";

function WordCloudGraph({ value, title }: SingleBarGraphParams) {
const theme = useTheme();
console.log(value);
return (
<Paper className="tile line">
<Grid container>
<Grid item xs={12}>
<Stack direction="row" spacing={1}>
<Typography variant="h6" color={theme.palette.secondary.dark}>
<b> {title} </b>
</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<ResponsiveContainer width="100%" height="100%">
<ReactWordcloud words={value} />
</ResponsiveContainer>
</Grid>
</Grid>
</Paper>
);
}

export default WordCloudGraph;
10 changes: 9 additions & 1 deletion client/src/components/statistics/statistics.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getPagesForInsights } from "../../services/pages.service";
import { getLabels } from "../../services/label.service";
import { getTags } from "../../services/tags.service";
import { useToolStore } from "../../states/global.store";
import { addEventLog } from "../../services/interactions.service";

const Filter = lazy(() => import("./filter/filter.component"));
const BehavioralDashboard = lazy(
Expand Down Expand Up @@ -120,7 +121,14 @@ function Statistics({ open }: SidebarParams) {
key={tabItem.name}
elevation={0}
className="main tabs"
onClick={() => setTab(tabItem.tab)}
onClick={() => {
setTab(tabItem.tab);
if ((import.meta.env.VITE_ENABLE_TRACKING as string) == "true") {
addEventLog({
location: "Insights - " + tabItem.name,
});
}
}}
sx={{
border: tabItem.tab === tab ? "solid 2px #7f7f7f" : 0,
}}
Expand Down
20 changes: 20 additions & 0 deletions client/src/services/interactions.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createEventLog, fetchEventLogs } from "../api/interaction.api";
import { EventLogBody } from "../types/interaction/interaction.types";

export const addEventLog = async (event: EventLogBody) => {
try {
const response = await createEventLog(event);
return response.data;
} catch (error: any) {
throw error;
}
};

export const getEventLogs = async () => {
try {
const response = await fetchEventLogs();
return response;
} catch (error: any) {
throw error;
}
};
3 changes: 3 additions & 0 deletions client/src/types/interaction/interaction.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type EventLogBody = {
location: string;
};
50 changes: 47 additions & 3 deletions server/chat/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,51 @@
from django.contrib import admin
from .models import Chat, Message, Label
from django.contrib.auth.models import User
import csv
from django.http import HttpResponse
from django.contrib import admin

def export_as_csv(modeladmin, request, queryset):
meta = modeladmin.model._meta
field_names = [field.name for field in meta.fields]

response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
writer = csv.writer(response)

writer.writerow(field_names)
for obj in queryset:
row = []
for field in field_names:
value = getattr(obj, field)
if isinstance(value, User): # Check if the field is a User instance
value = value.id # Replace the value with the user's ID
row.append(value)
writer.writerow(row)

return response

export_as_csv.short_description = "Export Selected as CSV"

class ModelsChatActions(admin.ModelAdmin):
list_display = [field.name for field in Chat._meta.fields]
actions = [export_as_csv]
search_fields = [field.name for field in Chat._meta.fields if field.name != 'id']
list_filter = [field.name for field in Chat._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')]

class ModelsMessagesActions(admin.ModelAdmin):
list_display = [field.name for field in Message._meta.fields]
actions = [export_as_csv]
search_fields = [field.name for field in Message._meta.fields if field.name != 'id']
list_filter = [field.name for field in Message._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')]

class ModelsLabelsActions(admin.ModelAdmin):
list_display = [field.name for field in Label._meta.fields]
actions = [export_as_csv]
search_fields = [field.name for field in Label._meta.fields if field.name != 'id']
list_filter = [field.name for field in Label._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')]

# Register your models here.
admin.site.register(Chat)
admin.site.register(Message)
admin.site.register(Label)
admin.site.register(Chat, ModelsChatActions)
admin.site.register(Message, ModelsMessagesActions)
admin.site.register(Label, ModelsLabelsActions)
8 changes: 8 additions & 0 deletions server/chat/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ def post(self, request, *args, **kwargs):
return Response(serializer.data, status=status.HTTP_201_CREATED)

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

def delete(self, request, pk, format=None):
try:
label = Label.objects.get(id=pk)
except Label.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
label.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

class ChatByPageDetailView(APIView):
permission_classes = [IsAuthenticated]
Expand Down
Empty file added server/interactions/__init__.py
Empty file.
37 changes: 37 additions & 0 deletions server/interactions/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from django.contrib import admin
from .models import EventLog
from django.contrib.auth.models import User
import csv
from django.http import HttpResponse
from django.contrib import admin

def export_as_csv(modeladmin, request, queryset):
meta = modeladmin.model._meta
field_names = [field.name for field in meta.fields]

response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
writer = csv.writer(response)

writer.writerow(field_names)
for obj in queryset:
row = []
for field in field_names:
value = getattr(obj, field)
if isinstance(value, User): # Check if the field is a User instance
value = value.id # Replace the value with the user's ID
row.append(value)
writer.writerow(row)

return response

export_as_csv.short_description = "Export Selected as CSV"

class ModelsEventLogActions(admin.ModelAdmin):
list_display = [field.name for field in EventLog._meta.fields]
actions = [export_as_csv]
search_fields = [field.name for field in EventLog._meta.fields if field.name != 'id']
list_filter = [field.name for field in EventLog._meta.fields if field.get_internal_type() in ('CharField', 'BooleanField', 'DateField', 'DateTimeField', 'ForeignKey', 'IntegerField')]

# Register your models here.
admin.site.register(EventLog, ModelsEventLogActions)
6 changes: 6 additions & 0 deletions server/interactions/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class InteractionsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'interactions'
27 changes: 27 additions & 0 deletions server/interactions/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.0.4 on 2024-08-26 10:30

import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='EventLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('location', models.CharField(max_length=100)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='eventlogs', to=settings.AUTH_USER_MODEL)),
],
),
]
18 changes: 18 additions & 0 deletions server/interactions/migrations/0002_alter_eventlog_created_at.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-08-26 13:43

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('interactions', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='eventlog',
name='created_at',
field=models.DateTimeField(auto_now_add=True),
),
]
Empty file.
11 changes: 11 additions & 0 deletions server/interactions/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone

class EventLog(models.Model):
location = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="eventlogs")

def __str__(self):
return self.location
9 changes: 9 additions & 0 deletions server/interactions/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.contrib.auth.models import User
from rest_framework import serializers
from .models import EventLog

class EventLogSerializer(serializers.ModelSerializer):
class Meta:
model = EventLog
fields = ["id", "location", "created_at"]
extra_kwargs = {"user": {"read_only": True}}
3 changes: 3 additions & 0 deletions server/interactions/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
6 changes: 6 additions & 0 deletions server/interactions/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.urls import path
from . import views

urlpatterns = [
path('event-logs/', views.EventLogView.as_view(), name='event_log'),
]
Loading

0 comments on commit 0b2c039

Please sign in to comment.