Fixed multiple things

This commit is contained in:
Mawoka
2023-06-09 13:38:36 +02:00
parent c220e3ae18
commit 09a09cf1cf
13 changed files with 170 additions and 37 deletions
+1
View File
@@ -40,6 +40,7 @@ minio = "*"
xxhash = "*" xxhash = "*"
arq = "*" arq = "*"
thumbhash-python = "==1.0.0" thumbhash-python = "==1.0.0"
python-magic = "*"
[dev-packages] [dev-packages]
coverage = "*" coverage = "*"
Generated
+9 -1
View File
@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "dc7977c3d6c0c72f41144ddb7bc0821c1bd34c856cbf2a93077c809e6b7a9048" "sha256": "c3de18e25234c98fd1b33d4df4464fd6e05a485416c76eb6c4f64e78c497af64"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -1357,6 +1357,14 @@
"index": "pypi", "index": "pypi",
"version": "==3.3.0" "version": "==3.3.0"
}, },
"python-magic": {
"hashes": [
"sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b",
"sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"
],
"index": "pypi",
"version": "==0.4.27"
},
"python-multipart": { "python-multipart": {
"hashes": [ "hashes": [
"sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132", "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132",
+11 -10
View File
@@ -8,6 +8,7 @@ from datetime import datetime
from typing import Optional from typing import Optional
import ormar import ormar
from ormar import ReferentialAction
from pydantic import BaseModel, Json, validator from pydantic import BaseModel, Json, validator
from enum import Enum from enum import Enum
from . import metadata, database from . import metadata, database
@@ -58,7 +59,7 @@ class FidoCredentials(ormar.Model):
id: bytes = ormar.LargeBinary(max_length=256) id: bytes = ormar.LargeBinary(max_length=256)
public_key: bytes = ormar.LargeBinary(max_length=256) public_key: bytes = ormar.LargeBinary(max_length=256)
sign_count: int = ormar.Integer() sign_count: int = ormar.Integer()
user: Optional[User] = ormar.ForeignKey(User) user: Optional[User] = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE)
class Meta: class Meta:
tablename = "fido_credentials" tablename = "fido_credentials"
@@ -68,7 +69,7 @@ class FidoCredentials(ormar.Model):
class ApiKey(ormar.Model): class ApiKey(ormar.Model):
key: str = ormar.String(max_length=48, min_length=48, primary_key=True) key: str = ormar.String(max_length=48, min_length=48, primary_key=True)
user: Optional[User] = ormar.ForeignKey(User) user: Optional[User] = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE)
class Meta: class Meta:
tablename = "api_keys" tablename = "api_keys"
@@ -82,7 +83,7 @@ class UserSession(ormar.Model):
""" """
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4()) id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4())
user: Optional[User] = ormar.ForeignKey(User) user: Optional[User] = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE)
session_key: str = ormar.String(unique=True, max_length=64) session_key: str = ormar.String(unique=True, max_length=64)
created_at: datetime = ormar.DateTime(default=datetime.now()) created_at: datetime = ormar.DateTime(default=datetime.now())
ip_address: str = ormar.String(max_length=100, nullable=True) ip_address: str = ormar.String(max_length=100, nullable=True)
@@ -181,7 +182,7 @@ class Quiz(ormar.Model):
description: str = ormar.Text(nullable=True) description: str = ormar.Text(nullable=True)
created_at: datetime = ormar.DateTime(default=datetime.now()) created_at: datetime = ormar.DateTime(default=datetime.now())
updated_at: datetime = ormar.DateTime(default=datetime.now()) updated_at: datetime = ormar.DateTime(default=datetime.now())
user_id: uuid.UUID = ormar.ForeignKey(User) user_id: uuid.UUID = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE)
questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False) questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False)
imported_from_kahoot: Optional[bool] = ormar.Boolean(default=False, nullable=True) imported_from_kahoot: Optional[bool] = ormar.Boolean(default=False, nullable=True)
cover_image: Optional[str] = ormar.Text(nullable=True, unique=False) cover_image: Optional[str] = ormar.Text(nullable=True, unique=False)
@@ -307,8 +308,8 @@ class GameInLobby(BaseModel):
class GameResults(ormar.Model): class GameResults(ormar.Model):
id: uuid.UUID = ormar.UUID(primary_key=True) id: uuid.UUID = ormar.UUID(primary_key=True)
quiz: uuid.UUID | Quiz = ormar.ForeignKey(Quiz) quiz: uuid.UUID | Quiz = ormar.ForeignKey(Quiz, ondelete=ReferentialAction.CASCADE)
user: uuid.UUID | User = ormar.ForeignKey(User) user: uuid.UUID | User = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE)
timestamp: datetime = ormar.DateTime(default=datetime.now(), nullable=False) timestamp: datetime = ormar.DateTime(default=datetime.now(), nullable=False)
player_count: int = ormar.Integer(nullable=False, default=0) player_count: int = ormar.Integer(nullable=False, default=0)
note: str | None = ormar.Text(nullable=True) note: str | None = ormar.Text(nullable=True)
@@ -334,7 +335,7 @@ class QuizTivity(ormar.Model):
id: uuid.UUID = ormar.UUID(primary_key=True) id: uuid.UUID = ormar.UUID(primary_key=True)
title: str = ormar.Text(nullable=False) title: str = ormar.Text(nullable=False)
created_at: datetime = ormar.DateTime(nullable=False, server_default=func.now()) created_at: datetime = ormar.DateTime(nullable=False, server_default=func.now())
user: User | None = ormar.ForeignKey(User) user: User | None = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE)
pages: list[QuizTivityPage] = ormar.JSON(nullable=False) pages: list[QuizTivityPage] = ormar.JSON(nullable=False)
class Meta: class Meta:
@@ -347,8 +348,8 @@ class QuizTivityShare(ormar.Model):
id: uuid.UUID = ormar.UUID(primary_key=True) id: uuid.UUID = ormar.UUID(primary_key=True)
name: str | None = ormar.Text(nullable=True) name: str | None = ormar.Text(nullable=True)
expire_at: datetime | None = ormar.DateTime(nullable=True) expire_at: datetime | None = ormar.DateTime(nullable=True)
quiztivity: QuizTivity | None = ormar.ForeignKey(QuizTivity) quiztivity: QuizTivity | None = ormar.ForeignKey(QuizTivity, ondelete=ReferentialAction.CASCADE)
user: User | None = ormar.ForeignKey(User) user: User | None = ormar.ForeignKey(User, ondelete=ReferentialAction.CASCADE)
class Meta: class Meta:
tablename = "quiztivityshares" tablename = "quiztivityshares"
@@ -386,7 +387,7 @@ class StorageItem(ormar.Model):
uploaded_at: datetime = ormar.DateTime(nullable=False, default=datetime.now()) uploaded_at: datetime = ormar.DateTime(nullable=False, default=datetime.now())
mime_type: str = ormar.Text(nullable=False) mime_type: str = ormar.Text(nullable=False)
hash: bytes | None = ormar.LargeBinary(nullable=True, min_length=16, max_length=16) hash: bytes | None = ormar.LargeBinary(nullable=True, min_length=16, max_length=16)
user: User | None = ormar.ForeignKey(User) user: User | None = ormar.ForeignKey(User, ondelete=ReferentialAction.SET_NULL)
size: int = ormar.BigInteger(nullable=False) size: int = ormar.BigInteger(nullable=False)
storage_path: str | None = ormar.Text(nullable=True) storage_path: str | None = ormar.Text(nullable=True)
deleted_at: datetime | None = ormar.DateTime(nullable=True, default=None) deleted_at: datetime | None = ormar.DateTime(nullable=True, default=None)
+9 -6
View File
@@ -65,12 +65,15 @@ async def generate_spreadsheet(quiz_results: dict, quiz: Quiz, player_fields: di
worksheet.write(i + 1, 1, question["time"]) worksheet.write(i + 1, 1, question["time"])
try: try:
async with ClientSession() as session, session.get(question["image"]) as response: async with ClientSession() as session, session.get(
img_data = BytesIO(await response.read()) f"{settings.root_address}/api/v1/storage/download/{question['image']}"
worksheet.insert_image(i + 1, 2, question["image"], {"image_data": img_data}) ) as response:
image = Image.open(img_data) if "image" in response.headers.get("Content-Type"):
worksheet.set_row(i + 1, image.height) img_data = BytesIO(await response.read())
worksheet.set_column(2, 2, image.width) worksheet.insert_image(i + 1, 2, question["image"], {"image_data": img_data})
image = Image.open(img_data)
worksheet.set_row(i + 1, image.height)
worksheet.set_column(2, 2, image.width)
except TypeError: except TypeError:
pass pass
answer_amount = len(answer_data) answer_amount = len(answer_data)
+29 -6
View File
@@ -1,6 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public # This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this # License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/. # file, You can obtain one at https://mozilla.org/MPL/2.0/.
import io
import json import json
import uuid import uuid
from datetime import datetime from datetime import datetime
@@ -10,10 +11,11 @@ from aiohttp import ClientSession
from fastapi import APIRouter, Depends, File, UploadFile, HTTPException from fastapi import APIRouter, Depends, File, UploadFile, HTTPException
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from classquiz.auth import get_current_user from classquiz.auth import get_current_user
from classquiz.config import storage, settings from classquiz.config import storage, settings, arq
from classquiz.db.models import Quiz, User from classquiz.db.models import Quiz, User, StorageItem
import gzip import gzip
import urllib.parse import urllib.parse
import magic
router = APIRouter() router = APIRouter()
settings = settings() settings = settings()
@@ -49,7 +51,9 @@ async def export_quiz(quiz_id: uuid.UUID, user: User = Depends(get_current_user)
for image_key in image_urls.keys(): for image_key in image_urls.keys():
bin_data = bin_data + image_delimiter + str(image_key).encode("utf-8") + image_index_delimiter bin_data = bin_data + image_delimiter + str(image_key).encode("utf-8") + image_index_delimiter
image_data = None image_data = None
async with ClientSession() as session, session.get(image_urls[image_key]) as resp: async with ClientSession() as session, session.get(
f"{settings.root_address}/api/v1/storage/download/{image_urls[image_key]}"
) as resp:
image_data = await resp.read() image_data = await resp.read()
bin_data = bin_data + image_data bin_data = bin_data + image_data
@@ -68,6 +72,8 @@ async def export_quiz(quiz_id: uuid.UUID, user: User = Depends(get_current_user)
@router.post("/") @router.post("/")
async def import_quiz(file: UploadFile = File(), user: User = Depends(get_current_user)): async def import_quiz(file: UploadFile = File(), user: User = Depends(get_current_user)):
if user.storage_used > settings.free_storage_limit:
raise HTTPException(status_code=409, detail="Storage limit reached")
data = await file.read() data = await file.read()
[split_data, images] = data.split(quiz_delimiter) [split_data, images] = data.split(quiz_delimiter)
decompressed_quiz = gzip.decompress(split_data) decompressed_quiz = gzip.decompress(split_data)
@@ -75,15 +81,32 @@ async def import_quiz(file: UploadFile = File(), user: User = Depends(get_curren
image_splits = images.split(image_delimiter) image_splits = images.split(image_delimiter)
quiz_id = uuid.uuid4() quiz_id = uuid.uuid4()
image_urls = {} image_urls = {}
print(len(data))
for image_split in image_splits: for image_split in image_splits:
res = image_split.split(image_index_delimiter) res = image_split.split(image_index_delimiter)
if len(res) != 2: if len(res) != 2:
continue continue
[index, image_data] = res [index, image_data] = res
print(len(image_data))
img_data = io.BytesIO(image_data)
mime_type = magic.from_buffer(img_data.read(2048), mime=True)
print(mime_type)
index = int(index.decode("utf-8")) index = int(index.decode("utf-8"))
image_name = f"{quiz_id}--{uuid.uuid4()}" file_id = uuid.uuid4()
await storage.upload(file_name=image_name, file_data=image_data) file_obj = StorageItem(
image = f"{settings.root_address}/api/v1/storage/download/{image_name}" id=file_id,
uploaded_at=datetime.now(),
mime_type=mime_type,
hash=None,
user=user,
size=0,
deleted_at=None,
alt_text=None,
)
await storage.upload(file_name=file_id.hex, file_data=img_data, mime_type=mime_type)
await file_obj.save()
await arq.enqueue_job("calculate_hash", file_id.hex)
image = file_id.hex
image_urls[index] = image image_urls[index] = image
quiz_dict["created_at"] = datetime.fromisoformat(quiz_dict["created_at"]) quiz_dict["created_at"] = datetime.fromisoformat(quiz_dict["created_at"])
quiz_dict["updated_at"] = datetime.fromisoformat(quiz_dict["updated_at"]) quiz_dict["updated_at"] = datetime.fromisoformat(quiz_dict["updated_at"])
+3 -1
View File
@@ -209,7 +209,9 @@ async def delete_quiz(quiz_id: str, user: User = Depends(get_current_user)):
for question in quiz.questions: for question in quiz.questions:
try: try:
if question["image"] is not None and not str(question["image"]).startswith("https://i.imgur.com/"): if question["image"] is not None and not str(question["image"]).startswith("https://i.imgur.com/"):
pics_to_delete.append(pic_name_regex.match(question["image"]).group(1)) old_image_to_delete = pic_name_regex.match(question["image"])
if old_image_to_delete is not None:
pics_to_delete.append(old_image_to_delete.group(1))
except KeyError: except KeyError:
pass pass
if len(pics_to_delete) != 0: if len(pics_to_delete) != 0:
+4 -2
View File
@@ -131,7 +131,7 @@ async def upload_file(file: UploadFile = File(), user: User = Depends(get_curren
deleted_at=None, deleted_at=None,
alt_text=None, alt_text=None,
) )
await storage.upload(file_name=file_id.hex, file_data=file.file) await storage.upload(file_name=file_id.hex, file_data=file.file, mime_type=file.content_type)
await file_obj.save() await file_obj.save()
await arq.enqueue_job("calculate_hash", file_id.hex) await arq.enqueue_job("calculate_hash", file_id.hex)
return PublicStorageItem.from_db_model(file_obj) return PublicStorageItem.from_db_model(file_obj)
@@ -157,7 +157,9 @@ async def upload_raw_file(request: Request, user: User = Depends(get_current_use
alt_text=None, alt_text=None,
) )
# https://github.com/VirusTotal/vt-py/issues/119#issuecomment-1261246867 # https://github.com/VirusTotal/vt-py/issues/119#issuecomment-1261246867
await storage.upload(file_name=file_id.hex, file_data=data_file._file) await storage.upload(
file_name=file_id.hex, file_data=data_file._file, mime_type=request.headers.get("Content-Type")
)
await file_obj.save() await file_obj.save()
await arq.enqueue_job("calculate_hash", file_id.hex) await arq.enqueue_job("calculate_hash", file_id.hex)
return PublicStorageItem.from_db_model(file_obj) return PublicStorageItem.from_db_model(file_obj)
+1
View File
@@ -227,6 +227,7 @@
<div class="flex w-full"> <div class="flex w-full">
<MediaComponent <MediaComponent
src={quiz_data.questions[selected_question].image} src={quiz_data.questions[selected_question].image}
muted={false}
css_classes="max-h-[20vh] object-cover mx-auto mb-8 w-auto" css_classes="max-h-[20vh] object-cover mx-auto mb-8 w-auto"
/> />
</div> </div>
@@ -5,11 +5,17 @@
--> -->
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment';
export let src: string; export let src: string;
export let css_classes = 'max-h-64 h-auto w-auto'; export let css_classes = 'max-h-64 h-auto w-auto';
export let muted = true;
let type: 'img' | 'video' | undefined = undefined; let type: 'img' | 'video' | undefined = undefined;
const get_media = async () => { const get_media = async () => {
if (!browser) {
return;
}
const res = await fetch(`/api/v1/storage/info/${src}`); const res = await fetch(`/api/v1/storage/info/${src}`);
console.log('Headers', res.headers); console.log('Headers', res.headers);
const fileType = res.headers.get('Content-Type'); const fileType = res.headers.get('Content-Type');
@@ -46,13 +52,11 @@
<video <video
class={css_classes} class={css_classes}
disablepictureinpicture disablepictureinpicture
disableremoteplayback
x-webkit-airplay="deny" x-webkit-airplay="deny"
controls controls
autoplay autoplay
loop loop
controlslist="nofullscreen,noremoteplayback" {muted}
muted
preload="metadata" preload="metadata"
> >
<source src="/api/v1/storage/download/{src}" /> <source src="/api/v1/storage/download/{src}" />
+4 -4
View File
@@ -11,6 +11,7 @@
import Spinner from '$lib/Spinner.svelte'; import Spinner from '$lib/Spinner.svelte';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import BrownButton from '$lib/components/buttons/brown.svelte'; import BrownButton from '$lib/components/buttons/brown.svelte';
import MediaComponent from '$lib/editor/MediaComponent.svelte';
export let question: Question; export let question: Question;
@@ -93,10 +94,9 @@
<h1 class="text-3xl text-center">{@html question.question}</h1> <h1 class="text-3xl text-center">{@html question.question}</h1>
{#if question.image !== null} {#if question.image !== null}
<div> <div>
<img <MediaComponent
src="/api/v1/storage/download/{question.image}" src={question.image}
class="max-h-[40vh] object-cover mx-auto mb-8 w-auto" css_classes="max-h-[40vh] object-cover mx-auto mb-8 w-auto"
alt="Content for Question"
/> />
</div> </div>
{/if} {/if}
+1 -1
View File
@@ -187,7 +187,7 @@
</div> </div>
<div class="flex relative my-2"> <div class="flex relative my-2">
<div class="w-full"> <div class="w-full">
<BrownButton disabled={stats.progress !== 1} on:click={upload_video}> <BrownButton disabled={status !== Status.CompressDone} on:click={upload_video}>
Upload {file_size_in_mi ? `${file_size_in_mi.toFixed(2)}Mi` : ''}</BrownButton Upload {file_size_in_mi ? `${file_size_in_mi.toFixed(2)}Mi` : ''}</BrownButton
> >
</div> </div>
@@ -13,6 +13,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Spinner from '$lib/Spinner.svelte'; import Spinner from '$lib/Spinner.svelte';
import GrayButton from '$lib/components/buttons/gray.svelte'; import GrayButton from '$lib/components/buttons/gray.svelte';
import MediaComponent from '$lib/editor/MediaComponent.svelte';
const tippy = createTippy({ const tippy = createTippy({
arrow: true, arrow: true,
@@ -189,10 +190,11 @@
<!-- </label>--> <!-- </label>-->
{#if question.image} {#if question.image}
<span> <span>
<img <MediaComponent
class="pl-8" css_classes="mx-auto"
src="/api/v1/storage/download/{question.image}" src={question.image}
alt="Not provided" alt="Not provided"
muted={true}
/> />
</span> </span>
{/if} {/if}
@@ -0,0 +1,86 @@
"""Added DeleteAction to models
Revision ID: a1841b1918b3
Revises: 89c4b5d547aa
Create Date: 2023-06-09 13:13:00.865587
"""
from alembic import op
import sqlalchemy as sa
import ormar
# revision identifiers, used by Alembic.
revision = "a1841b1918b3"
down_revision = "89c4b5d547aa"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
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_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_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.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_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"],
ondelete="CASCADE",
)
op.create_foreign_key(
"fk_quiztivityshares_users_id_user", "quiztivityshares", "users", ["user"], ["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_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"])
op.create_foreign_key(
"fk_quiztivityshares_quiztivitys_id_quiztivity", "quiztivityshares", "quiztivitys", ["quiztivity"], ["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_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_users_id_user", "game_results", "users", ["user"], ["id"])
op.create_foreign_key("fk_game_results_quiz_id_quiz", "game_results", "quiz", ["quiz"], ["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"])
# ### end Alembic commands ###