Added Ratins, Plays and Views to Quizzes

This commit is contained in:
Mawoka
2023-06-30 20:40:18 +02:00
parent deac567e19
commit 99232b442d
11 changed files with 603 additions and 31 deletions
+17
View File
@@ -180,6 +180,10 @@ class Quiz(ormar.Model):
background_color: str | None = ormar.Text(nullable=True, unique=False)
background_image: str | None = ormar.Text(nullable=True, unique=False)
kahoot_id: uuid.UUID | None = ormar.UUID(nullable=True, default=None)
likes: int = ormar.Integer(nullable=False, default=0, server_default="0")
dislikes: int = ormar.Integer(nullable=False, default=0, server_default="0")
plays: int = ormar.Integer(nullable=False, default=0, server_default="0")
views: int = ormar.Integer(nullable=False, default=0, server_default="0")
class Meta:
tablename = "quiz"
@@ -472,3 +476,16 @@ class Controller(ormar.Model):
tablename = "controller"
metadata = metadata
database = database
class Rating(ormar.Model):
id: uuid.UUID = ormar.UUID(primary_key=True)
user: uuid.UUID | User = ormar.ForeignKey(User)
positive: bool = ormar.Boolean(nullable=False)
created_at: datetime = ormar.DateTime(nullable=False, server_default=func.now())
quiz: uuid.UUID | Quiz = ormar.ForeignKey(Quiz)
class Meta:
tablename = "rating"
metadata = metadata
database = database
+43 -3
View File
@@ -1,12 +1,17 @@
# SPDX-FileCopyrightText: 2023 Marlon W (Mawoka)
#
# SPDX-License-Identifier: MPL-2.0
import enum
import uuid
from datetime import datetime
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Depends
from uuid import UUID
from classquiz.db.models import User, Quiz
from pydantic import BaseModel
from classquiz.auth import get_current_user
from classquiz.db.models import User, Quiz, Rating
router = APIRouter()
@@ -35,3 +40,38 @@ async def get_quizzes_from_user(user_id: UUID, imported: bool | None = None):
raise HTTPException(status_code=404, detail="no quizzes found")
else:
return quizzes
class RateQuizInputType(str, enum.Enum):
LIKE = "LIKE"
DISLIKE = "DISLIKE"
class RateQuizInput(BaseModel):
type: RateQuizInputType
@router.post("/rate/{quiz_id}")
async def rate_quiz(data: RateQuizInput, quiz_id: uuid.UUID, user: User = Depends(get_current_user)):
quiz = await Quiz.objects.get_or_none(id=quiz_id, public=True)
if quiz is None:
raise HTTPException(status_code=404, detail="Quiz not found")
rating = await Rating.objects.get_or_none(quiz=quiz, user=user)
positive = True
if data.type == RateQuizInputType.DISLIKE:
positive = False
if rating is not None and rating.positive == positive:
raise HTTPException(status_code=409, detail="Rating already submitted")
elif rating.positive != positive:
await rating.delete()
if rating.positive:
quiz.likes -= 1
else:
quiz.dislikes -= 1
rating = Rating(id=uuid.uuid4(), user=user, positive=positive, quiz=quiz, created_at=datetime.now())
await rating.save()
if positive:
quiz.likes += 1
else:
quiz.dislikes += 1
await quiz.update()
+9 -5
View File
@@ -56,18 +56,20 @@ class PublicQuizResponseUser(BaseModel):
class PublicQuizResponse(Quiz.get_pydantic()):
user_id: PublicQuizResponseUser
questions: list[QuizQuestion]
likes: int
dislikes: int
views: int
plays: int
@router.get("/get/public/{quiz_id}")
async def get_public_quiz(quiz_id: str):
try:
quiz_id = uuid.UUID(quiz_id)
except ValueError:
raise HTTPException(status_code=400, detail="badly formed quiz id")
async def get_public_quiz(quiz_id: uuid.UUID):
quiz = await Quiz.objects.select_related("user_id").get_or_none(id=quiz_id)
if quiz is None:
return JSONResponse(status_code=404, content={"detail": "quiz not found"})
else:
quiz.views += 1
await quiz.update()
return PublicQuizResponse(**quiz.dict())
@@ -89,6 +91,8 @@ async def start_quiz(
quiz = await Quiz.objects.get_or_none(id=quiz_id, public=True)
if quiz is None:
return JSONResponse(status_code=404, content={"detail": "quiz not found"})
quiz.plays += 1
await quiz.update()
game_pin = randint(100000, 999999)
if custom_field == "":
custom_field = None
+13 -2
View File
@@ -130,6 +130,7 @@
"screenshot_plural": "Screenshots",
"browser": "Browser",
"view": "View",
"view_plural": "Views",
"correct": "Correct",
"result": "Result",
"result_plural": "Results",
@@ -182,7 +183,15 @@
"files_library": "Files Library",
"answer_plural": "Answers",
"yes": "Yes",
"no": "no"
"no": "no",
"analytics": "Analytics",
"rating": "Rating",
"like": "Like",
"like_plural": "Likes",
"dislike": "Dislike",
"dislike_plural": "Dislikes",
"play_plural": "Plays",
"info": "Info"
},
"editor": {
"time_in_seconds": "Time in seconds",
@@ -281,7 +290,9 @@
"right_click_to_delete": "Right-click on an answer to delete it!"
},
"dashboard": {
"search_for_own_quizzes": "Search for your own quizzes"
"search_for_own_quizzes": "Search for your own quizzes",
"views_n_plays": "Views & Plays",
"info_analytics": "The \"Plays\" only show how often the quiz was started (you included), whereas the \"Views\" count how often the \"View\"-page was visited, so it also counts bots (sorry for that)."
},
"footer": {
"self_ads": "Made with ❤️ by {{mawoka_link}} and with the help of {{others_link}}.",
+4
View File
@@ -21,6 +21,10 @@ export interface QuizData {
cover_image?: string;
background_color?: string;
background_image?: string;
likes: number;
dislikes: number;
plays: number;
views: number;
}
export enum QuizQuestionType {
@@ -0,0 +1,21 @@
<!--
SPDX-FileCopyrightText: 2023 Marlon W (Mawoka)
SPDX-License-Identifier: MPL-2.0
-->
<script lang="ts">
export let hovering: boolean;
function enter() {
hovering = true;
}
function leave() {
hovering = false;
}
</script>
<div on:mouseenter={enter} on:mouseleave={leave}>
<slot {hovering} />
</div>
@@ -0,0 +1,154 @@
<!--
SPDX-FileCopyrightText: 2023 Marlon W (Mawoka)
SPDX-License-Identifier: MPL-2.0
-->
<script lang="ts">
import type { QuizData } from '$lib/quiz_types';
import Hoverable from '$lib/view_quiz/Hoverable.svelte';
import { createTippy } from 'svelte-tippy';
export let quiz: QuizData;
let FeedBackButtonsHovered = {
dislike: false,
like: false
};
const tippy = createTippy({
arrow: true,
animation: 'perspective-subtle',
placement: 'top'
});
const complete_action = async (positive: boolean) => {
const res = await fetch(`/api/v1/community/rate/${quiz.id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ type: positive ? 'LIKE' : 'DISLIKE' })
});
if (res.status === 409) {
alert("You've already rated this quiz!");
return;
} else if (!res.ok) {
return;
}
if (positive) {
quiz.likes += 1;
} else {
quiz.dislikes += 1;
}
};
</script>
<div class="flex flex-col border-[#B07156] rounded border-2 p-2 gap-2">
<div class="grid grid-cols-2 gap-2 group mx-auto">
<Hoverable bind:hovering={FeedBackButtonsHovered.like}>
<button
class="bg-green-500 rounded-full h-10 w-10 transition"
use:tippy={{ content: 'Like this quiz!' }}
class:opacity-40={FeedBackButtonsHovered.dislike}
on:click={() => complete_action(true)}
>
<!-- heroicons/thumb-up -->
<svg
class="inline-block h-6 w-6 align-middle text-black"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5"
/>
</svg>
</button>
</Hoverable>
<Hoverable bind:hovering={FeedBackButtonsHovered.dislike}>
<button
class="rounded-full bg-red-500 h-10 w-10 transition"
use:tippy={{ content: 'Dislike this quiz!' }}
class:opacity-40={FeedBackButtonsHovered.like}
on:click={() => complete_action(false)}
>
<!-- heroicons/thumb-down -->
<svg
class="inline-block h-6 w-6 align-middle text-black"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14H5.236a2 2 0 01-1.789-2.894l3.5-7A2 2 0 018.736 3h4.018a2 2 0 01.485.06l3.76.94m-7 10v5a2 2 0 002 2h.096c.5 0 .905-.405.905-.904 0-.715.211-1.413.608-2.008L17 13V4m-7 10h2m5-10h2a2 2 0 012 2v6a2 2 0 01-2 2h-2.5"
/>
</svg>
</button>
</Hoverable>
<span class="text-center">{quiz.likes}</span>
<span class="text-center">{quiz.dislikes}</span>
</div>
<span class="w-full border-t-2 border-[#B07156]" />
<div class="mx-auto grid grid-cols-2 gap-2">
<div class="flex flex-col">
<!-- heroicons/legacy-outline/Play -->
<svg
class="w-8 h-8"
use:tippy={{ content: 'How often the quiz was started' }}
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="mx-auto" use:tippy={{ content: 'How often the quiz was started' }}>
{quiz.plays}
</p>
</div>
<div class="flex flex-col">
<!-- heroicons/legacy-outline/Eye -->
<svg
class="w-8 h-8 mx-auto"
use:tippy={{ content: 'Quiz views' }}
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="mx-auto" use:tippy={{ content: 'Quiz views' }}>{quiz.views}</p>
</div>
</div>
</div>
+120 -18
View File
@@ -5,7 +5,7 @@ SPDX-License-Identifier: MPL-2.0
-->
<script lang="ts">
import type { Question } from '$lib/quiz_types';
import type { QuizData } from '$lib/quiz_types';
import { getLocalization } from '$lib/i18n';
import Footer from '$lib/footer.svelte';
import { navbarVisible, signedIn } from '$lib/stores';
@@ -15,20 +15,10 @@ SPDX-License-Identifier: MPL-2.0
import type { PageData } from './$types';
import { fly } from 'svelte/transition';
import StartGamePopup from '$lib/dashboard/start_game.svelte';
import Analytics from './Analytics.svelte';
// import GrayButton from "$lib/components/buttons/gray.svelte";
interface QuizData {
id: string;
public: boolean;
title: string;
description: string;
created_at: string;
updated_at: string;
user_id: string;
questions: Question[];
}
export let data: PageData;
let search_term = '';
let start_game = null;
@@ -96,12 +86,16 @@ SPDX-License-Identifier: MPL-2.0
window.location.reload();
};
let create_button_clicked = false;
let analytics_quiz_selected: undefined | QuizData = undefined;
</script>
<svelte:head>
<title>ClassQuiz - Dashboard</title>
</svelte:head>
{#if analytics_quiz_selected}
<Analytics bind:quiz={analytics_quiz_selected} />
{/if}
<div class="min-h-screen flex flex-col">
{#await getData()}
<svg class="h-8 w-8 animate-spin mx-auto my-20" viewBox="3 3 18 18">
@@ -198,25 +192,133 @@ SPDX-License-Identifier: MPL-2.0
</p>
</div>
<div
class="grid grid-cols-2 grid-rows-2 ml-auto gap-2 w-fit self-end my-auto"
class="grid grid-rows-2 ml-auto gap-2 w-fit self-end my-auto"
class:grid-cols-3={quiz.type === 'quiz'}
class:grid-cols-2={quiz.type !== 'quiztivity'}
>
<BrownButton
flex={true}
disabled={!quiz.public}
href="/view/{quiz.id}"
>
<!-- heroicons/legacy-outline/Eye -->
<svg
class="w-5 h-5"
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</BrownButton>
<BrownButton
flex={true}
on:click={() => (analytics_quiz_selected = quiz)}
>
<!-- heroicons/legacy-outline/ChartBar -->
<svg
class="w-5 h-5"
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</BrownButton>
<BrownButton
href={quiz.type === 'quiz'
? `/edit?quiz_id=${quiz.id}`
: `/quiztivity/edit?id=${quiz.id}`}
>{$t('words.edit')}</BrownButton
flex={true}
>
<!-- heroicons/legacy-outline/Pencil -->
<svg
class="w-5 h-5"
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</BrownButton>
{#if quiz.type === 'quiz'}
<BrownButton
on:click={() => {
start_game = quiz.id;
}}
flex={true}
>
{$t('words.start')}
<!-- heroicons/legacy-outline/Play -->
<svg
class="w-5 h-5"
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</BrownButton>
{:else}
<BrownButton href="/quiztivity/play?id={quiz.id}">
{$t('words.play')}
<BrownButton href="/quiztivity/play?id={quiz.id}" flex={true}>
<!-- heroicons/legacy-outline/Play -->
<svg
class="w-5 h-5"
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</BrownButton>
{/if}
@@ -0,0 +1,83 @@
<!--
SPDX-FileCopyrightText: 2023 Marlon W (Mawoka)
SPDX-License-Identifier: MPL-2.0
-->
<script lang="ts">
import type { QuizData } from '$lib/quiz_types';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
export let quiz: QuizData | undefined = undefined;
const on_parent_click = (e: Event) => {
if (e.target !== e.currentTarget) {
return;
}
quiz = undefined;
};
const close_start_game_if_esc_is_pressed = (key: KeyboardEvent) => {
if (key.code === 'Escape') {
quiz = undefined;
}
};
onMount(() => {
document.body.addEventListener('keydown', close_start_game_if_esc_is_pressed);
});
</script>
<div
class="fixed w-full h-full top-0 flex bg-black bg-opacity-50 z-50"
on:click={on_parent_click}
transition:fade|local={{ duration: 100 }}
>
<div class="m-auto bg-white dark:bg-gray-600 rounded shadow-2xl flex p-4 flex-col w-2/3 h-5/6">
<h1 class="text-center text-5xl">{$t('words.analytics')}</h1>
<section class="flex flex-col gap-2 mt-8">
<h2 class="mx-auto text-2xl">{$t('words.rating')}</h2>
<table class="w-fit mx-auto">
<tr class="border-b-2 dark:border-gray-500 text-left border-gray-300">
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
>{$t('words.like', { count: 2 })}</th
>
<th class="p-1 mx-auto">{$t('words.dislike', { count: 2 })}</th>
</tr>
<tr class="text-left">
<td class="border-r dark:border-gray-500 p-1 border-gray-300">{quiz.likes}</td>
<td class="mx-auto p-1">{quiz.dislikes}</td>
</tr>
</table>
</section>
<section class="flex flex-col gap-2 mt-8">
<h2 class="mx-auto text-2xl">{$t('dashboard.views_n_plays')}</h2>
<table class="w-fit mx-auto">
<tr class="border-b-2 dark:border-gray-500 text-left border-gray-300">
<th class="border-r dark:border-gray-500 p-1 mx-auto border-gray-300"
>{$t('words.view', { count: 2 })}</th
>
<th class="p-1 mx-auto">{$t('words.play', { count: 2 })}</th>
</tr>
<tr class="text-left">
<td class="border-r dark:border-gray-500 p-1 border-gray-300">{quiz.views}</td>
<td class="mx-auto p-1">{quiz.plays}</td>
</tr>
</table>
</section>
<section class="flex flex-col gap-2 mt-8">
<h2 class="mx-auto text-2xl">{$t('words.info')}</h2>
<p class="mx-auto max-w-[70%] text-center">
{$t('dashboard.info_analytics')}
</p>
</section>
<section class="mt-auto">
<p class="mt-6 mx-auto max-w-[70%] text-sm dark:text-gray-200 text-center">
Since there's still some space left down here, I guess that I take this opportunity
to thank You for using ClassQuiz! Have a great day and continue using ClassQuiz ;)
</p>
</section>
</div>
</div>
@@ -15,6 +15,7 @@ SPDX-License-Identifier: MPL-2.0
import Spinner from '$lib/Spinner.svelte';
import GrayButton from '$lib/components/buttons/gray.svelte';
import MediaComponent from '$lib/editor/MediaComponent.svelte';
import RatingComponent from '$lib/view_quiz/RatingComponent.svelte';
const tippy = createTippy({
arrow: true,
@@ -90,6 +91,9 @@ SPDX-License-Identifier: MPL-2.0
<div class="text-center text-sm pt-1 mb-4">
<ImportedOrNot imported={quiz.imported_from_kahoot} />
</div>
<div class="flex justify-center mb-2">
<RatingComponent bind:quiz />
</div>
<div class="flex flex-col justify-center">
<div class="mx-auto flex flex-col gap-2 justify-center w-fit">
{#if quiz.imported_from_kahoot && quiz.kahoot_id}
@@ -108,15 +112,56 @@ SPDX-License-Identifier: MPL-2.0
on:click={() => {
start_game = quiz.id;
}}
flex={true}
>
{$t('words.start')}
<!-- heroicons/legacy-outline/Play -->
<svg
class="w-5 h-5"
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</GrayButton>
</div>
{:else}
<div use:tippy={{ content: 'You need to be logged in to start a game' }}>
<div class="w-full">
<GrayButton disabled={true}>
{$t('words.start')}
<GrayButton disabled={true} flex={true}>
<!-- heroicons/legacy-outline/Play -->
<svg
class="w-5 h-5"
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</GrayButton>
</div>
</div>
@@ -0,0 +1,91 @@
# SPDX-FileCopyrightText: 2023 Marlon W (Mawoka)
#
# SPDX-License-Identifier: MPL-2.0
"""Added Ratings, Plays and Views to Quiz
Revision ID: 230ac26db527
Revises: 32649a1ffcf2
Create Date: 2023-06-30 19:18:02.881031
"""
from alembic import op
import sqlalchemy as sa
import ormar
# revision identifiers, used by Alembic.
revision = "230ac26db527"
down_revision = "32649a1ffcf2"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"rating",
sa.Column("id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=False),
sa.Column("user", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True),
sa.Column("positive", sa.Boolean(), nullable=False),
sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.Column("quiz", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True),
sa.ForeignKeyConstraint(["quiz"], ["quiz.id"], name="fk_rating_quiz_id_quiz"),
sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_rating_users_id_user"),
sa.PrimaryKeyConstraint("id"),
)
# op.drop_constraint('fk_api_keys_users_id_user', 'api_keys', type_='foreignkey')
# op.create_foreign_key('fk_api_keys_users_id_user', 'api_keys', 'users', ['user'], ['id'], ondelete='CASCADE')
# op.drop_constraint('fk_fido_credentials_users_id_user', 'fido_credentials', type_='foreignkey')
# op.create_foreign_key('fk_fido_credentials_users_id_user', 'fido_credentials', 'users', ['user'], ['id'], ondelete='CASCADE')
# op.drop_constraint('fk_game_results_users_id_user', 'game_results', type_='foreignkey')
# op.drop_constraint('fk_game_results_quiz_id_quiz', 'game_results', type_='foreignkey')
# op.create_foreign_key('fk_game_results_users_id_user', 'game_results', 'users', ['user'], ['id'], ondelete='CASCADE')
# op.create_foreign_key('fk_game_results_quiz_id_quiz', 'game_results', 'quiz', ['quiz'], ['id'], ondelete='CASCADE')
op.add_column("quiz", sa.Column("likes", sa.Integer(), server_default="0", nullable=False))
op.add_column("quiz", sa.Column("dislikes", sa.Integer(), server_default="0", nullable=False))
op.add_column("quiz", sa.Column("plays", sa.Integer(), server_default="0", nullable=False))
op.add_column("quiz", sa.Column("views", sa.Integer(), server_default="0", nullable=False))
# op.drop_constraint('fk_quiz_users_id_user_id', 'quiz', type_='foreignkey')
# op.create_foreign_key('fk_quiz_users_id_user_id', 'quiz', 'users', ['user_id'], ['id'], ondelete='CASCADE')
# op.drop_constraint('fk_quiztivitys_users_id_user', 'quiztivitys', type_='foreignkey')
# op.create_foreign_key('fk_quiztivitys_users_id_user', 'quiztivitys', 'users', ['user'], ['id'], ondelete='CASCADE')
# op.drop_constraint('fk_quiztivityshares_users_id_user', 'quiztivityshares', type_='foreignkey')
# op.drop_constraint('fk_quiztivityshares_quiztivitys_id_quiztivity', 'quiztivityshares', type_='foreignkey')
# op.create_foreign_key('fk_quiztivityshares_users_id_user', 'quiztivityshares', 'users', ['user'], ['id'], ondelete='CASCADE')
# op.create_foreign_key('fk_quiztivityshares_quiztivitys_id_quiztivity', 'quiztivityshares', 'quiztivitys', ['quiztivity'], ['id'], ondelete='CASCADE')
# op.drop_constraint('fk_storage_items_users_id_user', 'storage_items', type_='foreignkey')
# op.create_foreign_key('fk_storage_items_users_id_user', 'storage_items', 'users', ['user'], ['id'], ondelete='SET NULL')
# op.drop_constraint('fk_user_sessions_users_id_user', 'user_sessions', type_='foreignkey')
# op.create_foreign_key('fk_user_sessions_users_id_user', 'user_sessions', 'users', ['user'], ['id'], ondelete='CASCADE')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# op.drop_constraint('fk_user_sessions_users_id_user', 'user_sessions', type_='foreignkey')
# op.create_foreign_key('fk_user_sessions_users_id_user', 'user_sessions', 'users', ['user'], ['id'])
# op.drop_constraint('fk_storage_items_users_id_user', 'storage_items', type_='foreignkey')
# op.create_foreign_key('fk_storage_items_users_id_user', 'storage_items', 'users', ['user'], ['id'])
# op.drop_constraint('fk_quiztivityshares_quiztivitys_id_quiztivity', 'quiztivityshares', type_='foreignkey')
# op.drop_constraint('fk_quiztivityshares_users_id_user', 'quiztivityshares', type_='foreignkey')
# op.create_foreign_key('fk_quiztivityshares_quiztivitys_id_quiztivity', 'quiztivityshares', 'quiztivitys', ['quiztivity'], ['id'])
# op.create_foreign_key('fk_quiztivityshares_users_id_user', 'quiztivityshares', 'users', ['user'], ['id'])
# op.drop_constraint('fk_quiztivitys_users_id_user', 'quiztivitys', type_='foreignkey')
# op.create_foreign_key('fk_quiztivitys_users_id_user', 'quiztivitys', 'users', ['user'], ['id'])
# op.drop_constraint('fk_quiz_users_id_user_id', 'quiz', type_='foreignkey')
# op.create_foreign_key('fk_quiz_users_id_user_id', 'quiz', 'users', ['user_id'], ['id'])
op.drop_column("quiz", "views")
op.drop_column("quiz", "plays")
op.drop_column("quiz", "dislikes")
op.drop_column("quiz", "likes")
# op.drop_constraint('fk_game_results_quiz_id_quiz', 'game_results', type_='foreignkey')
# op.drop_constraint('fk_game_results_users_id_user', 'game_results', type_='foreignkey')
# op.create_foreign_key('fk_game_results_quiz_id_quiz', 'game_results', 'quiz', ['quiz'], ['id'])
# op.create_foreign_key('fk_game_results_users_id_user', 'game_results', 'users', ['user'], ['id'])
# op.drop_constraint('fk_fido_credentials_users_id_user', 'fido_credentials', type_='foreignkey')
# op.create_foreign_key('fk_fido_credentials_users_id_user', 'fido_credentials', 'users', ['user'], ['id'])
# op.drop_constraint('fk_api_keys_users_id_user', 'api_keys', type_='foreignkey')
# op.create_foreign_key('fk_api_keys_users_id_user', 'api_keys', 'users', ['user'], ['id'])
op.drop_table("rating")
# ### end Alembic commands ###