new: [app] Added various improvements such as admin panel, enabled exercises, ...

This commit is contained in:
Sami Mokaddem 2024-07-01 11:21:01 +02:00
parent c9d90283eb
commit efa4d7613b
13 changed files with 284 additions and 45 deletions

3
db.py
View file

@ -5,7 +5,8 @@ import collections
USER_ID_TO_EMAIL_MAPPING = {}
USER_ID_TO_AUTHKEY_MAPPING = {}
ALL_EXERCICES = []
ALL_EXERCISES = []
SELECTED_EXERCISES = []
INJECT_BY_UUID = {}
INJECT_SEQUENCE_BY_INJECT_UUID = {}
INJECT_REQUIREMENTS_BY_INJECT_UUID = {}

View file

@ -14,11 +14,9 @@ ACTIVE_EXERCISES_DIR = "active_exercises"
def load_exercises() -> bool:
db.ALL_EXERCICES = read_exercise_dir()
db.ALL_EXERCISES = read_exercise_dir()
init_inject_flow()
init_exercises_tasks()
# mark_task_completed(10, "4703a4b2-0ae4-47f3-9dc3-91250be60156", "e2216993-6192-4e7c-ae30-97cfe9de61b4") # filtering - past 48h
# mark_task_completed(10, "29324587-db6c-4a73-a209-cf8c79871629", "a6b5cf88-ba93-4c3f-8265-04e00d53778e") # Data - Event creation
return True
@ -35,12 +33,12 @@ def read_exercise_dir():
def init_inject_flow():
for exercise in db.ALL_EXERCICES:
for exercise in db.ALL_EXERCISES:
for inject in exercise['injects']:
inject['exercise_uuid'] = exercise['exercise']['uuid']
db.INJECT_BY_UUID[inject['uuid']] = inject
for exercise in db.ALL_EXERCICES:
for exercise in db.ALL_EXERCISES:
for inject_flow in exercise['inject_flow']:
db.INJECT_REQUIREMENTS_BY_INJECT_UUID[inject_flow['inject_uuid']] = inject_flow['requirements']
db.INJECT_SEQUENCE_BY_INJECT_UUID[inject_flow['inject_uuid']] = []
@ -49,7 +47,7 @@ def init_inject_flow():
def init_exercises_tasks():
for exercise in db.ALL_EXERCICES:
for exercise in db.ALL_EXERCISES:
tasks = {}
for inject in exercise['injects']:
tasks[inject['uuid']] = {
@ -65,8 +63,8 @@ def init_exercises_tasks():
def get_exercises():
exercices = []
for exercise in db.ALL_EXERCICES:
exercises = []
for exercise in db.ALL_EXERCISES:
tasks = []
for inject in exercise['injects']:
score = 0
@ -82,7 +80,7 @@ def get_exercises():
"score": score,
}
)
exercices.append(
exercises.append(
{
"name": exercise['exercise']['name'],
"uuid": exercise['exercise']['uuid'],
@ -92,8 +90,28 @@ def get_exercises():
"tasks": tasks,
}
)
exercices = sorted(exercices, key=lambda d: d['priority'])
return exercices
exercises = sorted(exercises, key=lambda d: d['priority'])
return exercises
def get_selected_exercises():
return db.SELECTED_EXERCISES
def change_exercise_selection(exercise_uuid: str, selected: bool):
if selected:
if exercise_uuid not in db.SELECTED_EXERCISES:
db.SELECTED_EXERCISES.append(exercise_uuid)
else:
if exercise_uuid in db.SELECTED_EXERCISES:
db.SELECTED_EXERCISES.remove(exercise_uuid)
def resetAllExerciseProgress():
for user_id in db.USER_ID_TO_EMAIL_MAPPING.keys():
for exercise_status in db.EXERCISES_STATUS.values():
for task in exercise_status['tasks'].values():
mark_task_incomplete(user_id, exercise_status['uuid'], task['uuid'])
def get_completed_tasks_for_user(user_id: int):
@ -338,6 +356,9 @@ def check_active_tasks(user_id: int, data: dict, context: dict) -> bool:
available_tasks = get_available_tasks_for_user(user_id)
for task_uuid in available_tasks:
inject = db.INJECT_BY_UUID[task_uuid]
if inject['exercise_uuid'] not in db.SELECTED_EXERCISES:
print(f'exercise not active for this inject {inject['name']}')
continue
print(f'checking: {inject['name']}')
completed = check_inject(user_id, inject, data, context)
if completed:

45
package-lock.json generated
View file

@ -20,6 +20,7 @@
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"autoprefixer": "^10.4.19",
"daisyui": "^4.12.10",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"postcss": "^8.4.38",
@ -1419,6 +1420,16 @@
"node": ">= 8"
}
},
"node_modules/css-selector-tokenizer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz",
"integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"fastparse": "^1.1.2"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -1436,6 +1447,34 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/culori": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
"integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/daisyui": {
"version": "4.12.10",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.10.tgz",
"integrity": "sha512-jp1RAuzbHhGdXmn957Z2XsTZStXGHzFfF0FgIOZj3Wv9sH7OZgLfXTRZNfKVYxltGUOBsG1kbWAdF5SrqjebvA==",
"dev": true,
"dependencies": {
"css-selector-tokenizer": "^0.8",
"culori": "^3",
"picocolors": "^1",
"postcss-js": "^4"
},
"engines": {
"node": ">=16.9.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/daisyui"
}
},
"node_modules/debug": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
@ -1856,6 +1895,12 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"node_modules/fastparse": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
"integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
"dev": true
},
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",

View file

@ -23,6 +23,7 @@
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"autoprefixer": "^10.4.19",
"daisyui": "^4.12.10",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"postcss": "^8.4.38",

View file

@ -40,6 +40,14 @@ def disconnect(sid):
def get_exercises(sid):
return exercise_model.get_exercises()
@sio.event
def get_selected_exercises(sid):
return exercise_model.get_selected_exercises()
@sio.event
def change_exercise_selection(sid, payload):
return exercise_model.change_exercise_selection(payload['exercise_uuid'], payload['selected'])
@sio.event
def get_progress(sid):
return exercise_model.get_progress()
@ -50,11 +58,15 @@ def get_notifications(sid):
@sio.event
def mark_task_completed(sid, payload):
return exercise_model.mark_task_completed(payload['user_id'], payload['exercise_uuid'], payload['task_uuid'])
return exercise_model.mark_task_completed(int(payload['user_id']), payload['exercise_uuid'], payload['task_uuid'])
@sio.event
def mark_task_incomplete(sid, payload):
return exercise_model.mark_task_incomplete(payload['user_id'], payload['exercise_uuid'], payload['task_uuid'])
return exercise_model.mark_task_incomplete(int(payload['user_id']), payload['exercise_uuid'], payload['task_uuid'])
@sio.event
def reset_all_exercise_progress(sid):
return exercise_model.resetAllExerciseProgress()
@sio.on('*')
def any_event(event, sid, data={}):

View file

@ -1,8 +1,9 @@
<script setup>
import { onMounted } from 'vue'
import TheDahboard from './TheDahboard.vue'
import TheThemeButton from './components/TheThemeButton.vue'
import TheAdminPanel from './components/TheAdminPanel.vue'
import TheSocketConnectionState from './components/TheSocketConnectionState.vue'
import TheDahboard from './TheDahboard.vue'
import { socketConnected } from "@/socket";
@ -16,9 +17,12 @@ onMounted(() => {
<template>
<main>
<div class="absolute top-1 right-1">
<div class="flex gap-2">
<TheThemeButton></TheThemeButton>
<TheAdminPanel></TheAdminPanel>
<TheSocketConnectionState></TheSocketConnectionState>
</div>
</div>
<TheDahboard></TheDahboard>
</main>
</template>

View file

@ -0,0 +1,71 @@
<script setup>
import { ref } from 'vue'
import { exercises, selected_exercises, resetAllExerciseProgress, changeExerciseSelection } from "@/socket";
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faScrewdriverWrench, faTrash } from '@fortawesome/free-solid-svg-icons'
const admin_modal = ref(null)
function changeSelectionState(state_enabled, exec_uuid) {
changeExerciseSelection(exec_uuid, state_enabled);
}
</script>
<template>
<button
@click="admin_modal.showModal()"
class="px-2 py-1 rounded-md focus-outline font-semibold bg-blue-600 text-slate-200 hover:bg-blue-700"
>
<FontAwesomeIcon :icon="faScrewdriverWrench" class="mr-1"></FontAwesomeIcon>
Admin panel
</button>
<dialog ref="admin_modal" class="modal">
<div class="modal-box w-11/12 max-w-6xl top-24 absolute bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-200">
<h2 class="text-2xl font-bold">
<FontAwesomeIcon :icon="faScrewdriverWrench" class="mr-1"></FontAwesomeIcon>
Admin panel
</h2>
<div class="modal-action">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
</div>
<div>
<div class="flex mb-5">
<button
@click="resetAllExerciseProgress()"
class="px-2 py-1 rounded-md focus-outline font-semibold bg-red-600 text-slate-200 hover:bg-red-700"
>
<FontAwesomeIcon :icon="faTrash" class="mr-1"></FontAwesomeIcon>
Reset All Exercises
</button>
</div>
<h3 class="text-lg font-semibold">Selected Exercises</h3>
<div
v-for="(exercise) in exercises"
:key="exercise.name"
class="form-control pl-3"
>
<label class="label cursor-pointer justify-start">
<input
@change="changeSelectionState($event.target.checked, exercise.uuid)"
type="checkbox"
:checked="selected_exercises.includes(exercise.uuid)"
:value="exercise.uuid"
:class="`checkbox ${selected_exercises.includes(exercise.uuid) ? 'checkbox-success' : ''}`"
/>
<span class="font-mono font-semibold text-base ml-3">{{ exercise.name }}</span>
</label>
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</template>

View file

@ -57,7 +57,7 @@
</tr>
</thead>
<tbody>
<tr v-if="Object.keys(notifications).length == 0">
<tr v-if="notifications.length == 0">
<td
colspan="5"
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6"
@ -89,6 +89,10 @@
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
dark:bg-amber-600 dark:text-neutral-100 bg-amber-600 text-neutral-100"
>POST</span>
<span v-else-if="notification.http_method == 'PUT'"
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
dark:bg-amber-600 dark:text-neutral-100 bg-amber-600 text-neutral-100"
>PUT</span>
<span v-else-if="notification.http_method == 'DELETE'"
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
dark:bg-red-600 dark:text-neutral-100 bg-red-600 text-neutral-100"

View file

@ -1,23 +1,17 @@
<script setup>
import { exercises, progresses } from "@/socket";
import { computed } from "vue";
import { active_exercises as exercises, progresses, setCompletedState } from "@/socket";
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faCheck, faTimes, faGraduationCap } from '@fortawesome/free-solid-svg-icons'
function toggle_completed(completed, user_id, exec_uuid, task_uuid) {
const payload = {
user_id: user_id,
exercise_uuid: exec_uuid,
task_uuid: task_uuid,
}
const event_name = !completed ? "mark_task_completed": "mark_task_incomplete"
socket.emit(event_name, payload, () => {
socket.emit("get_progress", (all_progress) => {
socketState.progresses = all_progress
})
})
function toggleCompleted(completed, user_id, exec_uuid, task_uuid) {
setCompletedState(completed, user_id, exec_uuid, task_uuid)
}
const hasExercises = computed(() => exercises.value.length > 0)
const hasProgress = computed(() => Object.keys(progresses.value).length > 0)
</script>
<template>
@ -25,6 +19,13 @@
<FontAwesomeIcon :icon="faGraduationCap"></FontAwesomeIcon>
Active Exercises
</h3>
<div
v-if="!hasExercises"
class="text-center text-slate-600 dark:text-slate-400 p-3 pl-6"
>
<i>- No Exercise available -</i>
</div>
<table
v-for="(exercise, exercise_index) in exercises"
:key="exercise.name"
@ -63,7 +64,7 @@
</tr>
</thead>
<tbody>
<tr v-if="Object.keys(progresses).length == 0">
<tr v-if="!hasProgress">
<td
:colspan="2 + exercise.tasks.length"
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6"
@ -85,8 +86,8 @@
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-500 dark:text-slate-400 p-3"
>
<span
class="select-none cursor-pointer"
@click="toggle_completed(progress.exercises[exercise.uuid].tasks_completion[task.uuid], user_id, exercise.uuid, task.uuid)"
class="select-none cursor-pointer text-nowrap"
@click="toggleCompleted(progress.exercises[exercise.uuid].tasks_completion[task.uuid], user_id, exercise.uuid, task.uuid)"
>
<FontAwesomeIcon
:icon="progress.exercises[exercise.uuid].tasks_completion[task.uuid] ? faCheck : faTimes"

View file

@ -1,6 +1,4 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
import { socketConnected } from "@/socket";
</script>

View file

@ -15,12 +15,41 @@
</script>
<template>
<button
<div class="flex">
<label class="grid cursor-pointer place-items-center">
<input
type="checkbox"
@click="darkMode = !darkMode"
class="mr-3 px-2 py-1 rounded-md focus-outline font-semibold bg-blue-600 text-slate-200 hover:bg-blue-700"
>
<FontAwesomeIcon :icon="faSun" class="mr-1" v-show="!darkMode"></FontAwesomeIcon>
<FontAwesomeIcon :icon="faMoon" class="mr-1" v-show="darkMode"></FontAwesomeIcon>
{{ darkMode ? 'Dark' : 'Light'}}
</button>
:checked="darkMode"
class="toggle theme-controller bg-slate-400 col-span-2 col-start-1 row-start-1 [--tglbg:#e2e8f0]" />
<svg
class="stroke-base-100 fill-base-100 col-start-1 row-start-1"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5" />
<path
d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />
</svg>
<svg
class="stroke-base-100 fill-base-100 col-start-2 row-start-1"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</label>
</div>
</template>

View file

@ -10,6 +10,7 @@ const initial_state = {
notificationCounter: 0,
notificationAPICounter: 0,
exercises: [],
selected_exercises: [],
progresses: {},
}
@ -19,6 +20,8 @@ const connectionState = reactive({
})
export const exercises = computed(() => state.exercises)
export const selected_exercises = computed(() => state.selected_exercises)
export const active_exercises = computed(() => state.exercises.filter((exercise) => state.selected_exercises.includes(exercise.uuid)))
export const progresses = computed(() => state.progresses)
export const notifications = computed(() => state.notificationEvents)
export const notificationCounter = computed(() => state.notificationCounter)
@ -34,6 +37,9 @@ export function fullReload() {
socket.emit("get_exercises", (all_exercises) => {
state.exercises = all_exercises
})
socket.emit("get_selected_exercises", (all_selected_exercises) => {
state.selected_exercises = all_selected_exercises
})
socket.emit("get_notifications", (all_notifications) => {
state.notificationEvents = all_notifications
})
@ -42,6 +48,40 @@ export function fullReload() {
})
}
export function setCompletedState(completed, user_id, exec_uuid, task_uuid) {
const payload = {
user_id: user_id,
exercise_uuid: exec_uuid,
task_uuid: task_uuid,
}
const event_name = !completed ? "mark_task_completed": "mark_task_incomplete"
socket.emit(event_name, payload, () => {
socket.emit("get_progress", (all_progress) => {
state.progresses = all_progress
})
})
}
export function resetAllExerciseProgress() {
socket.emit("reset_all_exercise_progress", () => {
socket.emit("get_progress", (all_progress) => {
state.progresses = all_progress
})
})
}
export function changeExerciseSelection(exec_uuid, state_enabled) {
const payload = {
exercise_uuid: exec_uuid,
selected: state_enabled,
}
socket.emit("change_exercise_selection", payload, () => {
socket.emit("get_selected_exercises", (all_selected_exercises) => {
state.selected_exercises = all_selected_exercises
})
})
}
const socket = io(URL, {
autoConnect: true

View file

@ -11,6 +11,18 @@ export default {
} ,
},
},
plugins: [],
plugins: [
require('daisyui'),
],
darkMode: ['selector'],
daisyui: {
themes: false, // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"]
darkTheme: "dark", // name of one of the included themes for dark mode
base: false, // applies background color and foreground color for root element by default
styled: true, // include daisyUI colors and design decisions for all components
utils: false, // adds responsive and modifier utility classes
prefix: "", // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors)
logs: false, // Shows info about daisyUI version and used config in the console when building your CSS
themeRoot: ":root", // The element that receives theme color CSS variables
},
}