✨ Added shares on admin-site
This commit is contained in:
@@ -338,3 +338,39 @@ class QuizTivity(ormar.Model):
|
||||
tablename = "quiztivitys"
|
||||
metadata = metadata
|
||||
database = database
|
||||
|
||||
|
||||
class QuizTivityShare(ormar.Model):
|
||||
id: uuid.UUID = ormar.UUID(primary_key=True)
|
||||
name: str | None = ormar.Text(nullable=True)
|
||||
expire_at: datetime | None = ormar.DateTime(nullable=True)
|
||||
quiztivity: QuizTivity | None = ormar.ForeignKey(QuizTivity)
|
||||
user: User | None = ormar.ForeignKey(User)
|
||||
|
||||
class Meta:
|
||||
tablename = "quiztivityshares"
|
||||
metadata = metadata
|
||||
database = database
|
||||
|
||||
|
||||
class OnlyId(BaseModel):
|
||||
id: uuid.UUID
|
||||
|
||||
|
||||
class PublicQuizTivityShare(BaseModel):
|
||||
id: uuid.UUID
|
||||
name: str | None
|
||||
expire_in: int
|
||||
quiztivity: OnlyId
|
||||
user: OnlyId
|
||||
|
||||
@classmethod
|
||||
def from_db_model(cls, data: QuizTivityShare):
|
||||
expire_in = int((data.expire_at - datetime.now()).seconds / 60)
|
||||
return cls(
|
||||
id=data.id,
|
||||
name=data.name,
|
||||
expire_in=expire_in,
|
||||
quiztivity=OnlyId(id=data.quiztivity.id),
|
||||
user=OnlyId(id=data.user.id),
|
||||
)
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
# 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
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from classquiz.auth import get_current_user
|
||||
from datetime import datetime
|
||||
from classquiz.db.models import User, QuizTivityInput, QuizTivity
|
||||
from classquiz.db.models import User, QuizTivityInput, QuizTivity, QuizTivityShare, PublicQuizTivityShare
|
||||
from classquiz.routers.quiztivity.shares import router as shares_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(shares_router, prefix="/shares")
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_quiztivity(data: QuizTivityInput, user: User = Depends(get_current_user)) -> QuizTivity:
|
||||
@@ -48,3 +52,12 @@ async def delete_quiztivity(uuid: UUID):
|
||||
async def get_all_quiztivities(user: User = Depends(get_current_user)) -> list[QuizTivity]:
|
||||
quiztivities = await QuizTivity.objects.filter(user=user).order_by(QuizTivity.created_at.desc()).all()
|
||||
return quiztivities
|
||||
|
||||
|
||||
@router.get("/{uuid}/shares")
|
||||
async def get_shares(uuid: UUID, user: User = Depends(get_current_user)) -> list[PublicQuizTivityShare]:
|
||||
shares = await QuizTivityShare.objects.filter(quiztivity=uuid, user=user).all()
|
||||
resp_shares = []
|
||||
for share in shares:
|
||||
resp_shares.append(PublicQuizTivityShare.from_db_model(share))
|
||||
return resp_shares
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from classquiz.auth import get_current_user
|
||||
from classquiz.db.models import User, QuizTivityShare, QuizTivity, PublicQuizTivityShare
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_shares(user: User = Depends(get_current_user)) -> list[PublicQuizTivityShare]:
|
||||
shares = await QuizTivityShare.objects.filter(user=user).all()
|
||||
resp_shares = []
|
||||
for share in shares:
|
||||
resp_shares.append(PublicQuizTivityShare.from_db_model(share))
|
||||
return resp_shares
|
||||
|
||||
|
||||
class CreateShareInput(BaseModel):
|
||||
name: str | None
|
||||
quiztivity: UUID
|
||||
expire_in: int | None
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_share(data: CreateShareInput, user: User = Depends(get_current_user)) -> PublicQuizTivityShare:
|
||||
quiztivity = await QuizTivity.objects.get_or_none(id=data.quiztivity, user=user)
|
||||
expire_at = None
|
||||
if data.expire_in is not None:
|
||||
expire_at = (datetime.now() + timedelta(minutes=data.expire_in)).replace(tzinfo=None)
|
||||
if quiztivity is None:
|
||||
raise HTTPException(status_code=400, detail="Quiztivity wasn't found")
|
||||
share = await QuizTivityShare.objects.create(
|
||||
id=uuid4(), name=data.name, expire_at=expire_at, quiztivity=quiztivity, user=user
|
||||
)
|
||||
share = PublicQuizTivityShare.from_db_model(share)
|
||||
return share
|
||||
|
||||
|
||||
@router.delete("/{uuid}")
|
||||
async def delete_share(uuid: UUID, user: User = Depends(get_current_user)):
|
||||
share = await QuizTivityShare.objects.get_or_none(id=uuid, user=user)
|
||||
if share is None:
|
||||
raise HTTPException(status_code=404, detail="Share wasn't found")
|
||||
await share.delete()
|
||||
return
|
||||
|
||||
|
||||
class UpdateShareInput(BaseModel):
|
||||
name: str | None
|
||||
expire_in: int | None
|
||||
|
||||
|
||||
@router.put("/{uuid}")
|
||||
async def update_share(
|
||||
data: UpdateShareInput, uuid: UUID, user: User = Depends(get_current_user)
|
||||
) -> PublicQuizTivityShare:
|
||||
share = await QuizTivityShare.objects.get_or_none(id=uuid, user=user)
|
||||
if share is None:
|
||||
raise HTTPException(status_code=404, detail="Share wasn't found")
|
||||
expire_at = None
|
||||
if data.expire_in is not None:
|
||||
expire_at = (datetime.now() + timedelta(minutes=data.expire_in)).replace(tzinfo=None)
|
||||
share.name = data.name
|
||||
share.expire_at = expire_at
|
||||
return PublicQuizTivityShare.from_db_model(await share.update())
|
||||
|
||||
|
||||
@router.get("/{uuid}")
|
||||
async def get_share(uuid: UUID) -> QuizTivity:
|
||||
share = await QuizTivityShare.objects.select_related("quiztivity").get_or_none(id=uuid)
|
||||
if share is None:
|
||||
raise HTTPException(status_code=404, detail="Share not found")
|
||||
return share.quiztivity
|
||||
@@ -0,0 +1,61 @@
|
||||
<!--
|
||||
- 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
|
||||
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import { PopoverTypes } from './smalltop';
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
export let open = false;
|
||||
export let type: PopoverTypes;
|
||||
export let data: undefined | { game_pin: number | string; game_id: string } = undefined;
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div class="fixed w-screen top-10 z-[60] flex justify-center" transition:fly={{ y: -100 }}>
|
||||
<div
|
||||
class="flex items-center p-4 w-full max-w-xs text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"
|
||||
role="alert"
|
||||
>
|
||||
<div class="ml-3 text-sm font-normal">
|
||||
{#if type === PopoverTypes.Copy}{$t('components.popover.copied_to_clipboard')}Copied
|
||||
to clipboard!
|
||||
{:else if type === PopoverTypes.GameInLobby}A game is currently in the lobby. Click <a
|
||||
class="underline"
|
||||
href="/remote?game_pin={data.game_pin}&game_id={data.game_id}">here</a
|
||||
> to join as a remote.
|
||||
{:else}
|
||||
<p>Error!!!</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||
data-dismiss-target="#toast-default"
|
||||
aria-label="Close"
|
||||
on:click={() => {
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
<span class="sr-only">{$t('words.close')}</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* 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
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum PopoverTypes {
|
||||
Copy,
|
||||
GameInLobby
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
import MarkdownEdit from './components/markdown/edit.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import SharesPopover from '$lib/quiztivity/shares_popover.svelte';
|
||||
|
||||
const { t } = getLocalization();
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -24,6 +25,7 @@
|
||||
let selected_slide = null;
|
||||
let opened_slide = null;
|
||||
let selected_type = undefined;
|
||||
let shares_menu_open = false;
|
||||
|
||||
for (let i = 0; i < data.pages.length; i++) {
|
||||
const id = (Math.random() + 1).toString(36).substring(7);
|
||||
@@ -67,7 +69,17 @@
|
||||
{#if opened_slide === null}
|
||||
<div>
|
||||
<div class="grid grid-cols-3">
|
||||
<span />
|
||||
{#if data.id}
|
||||
<div class="mr-auto w-fit pl-2">
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
shares_menu_open = true;
|
||||
}}>{$t('quiztivity.editor.open_shares_menu')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<span />
|
||||
{/if}
|
||||
<input
|
||||
class="bg-transparent outline-none text-center mx-auto"
|
||||
placeholder={$t('quiztivity.editor.title_placeholder')}
|
||||
@@ -166,3 +178,6 @@
|
||||
{#if selected_type === null}
|
||||
<AddNewSlide bind:type={selected_type} />
|
||||
{/if}
|
||||
{#if shares_menu_open}
|
||||
<SharesPopover id={data.id} bind:open={shares_menu_open} />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<!--
|
||||
- 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
|
||||
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import BrownButton from '$lib/components/buttons/brown.svelte';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import Spinner from '$lib/Spinner.svelte';
|
||||
import { DateTime } from 'luxon';
|
||||
import { browser } from '$app/environment';
|
||||
import SmallPopover from '$lib/components/popover/smalltop.svelte';
|
||||
import { PopoverTypes } from '$lib/components/popover/smalltop';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const { t } = getLocalization();
|
||||
export let open = false;
|
||||
export let id;
|
||||
let popover_open = false;
|
||||
const load_shares = async (): Promise<{
|
||||
id: string;
|
||||
name?: string;
|
||||
expire_in?: number;
|
||||
quiztivity: { id: string };
|
||||
user: { id: string };
|
||||
}> => {
|
||||
const res = await fetch(`/api/v1/quiztivity/${id}/shares`);
|
||||
return await res.json();
|
||||
};
|
||||
const copyToClipboard = (str) => {
|
||||
try {
|
||||
navigator.clipboard.writeText(str);
|
||||
} catch {
|
||||
console.log('Async Clipboard not supported');
|
||||
const el = document.createElement('textarea');
|
||||
el.value = str;
|
||||
el.setAttribute('readonly', '');
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = '-9999px';
|
||||
document.body.appendChild(el);
|
||||
const selected =
|
||||
document.getSelection().rangeCount > 0
|
||||
? document.getSelection().getRangeAt(0)
|
||||
: false;
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
if (selected) {
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(<Range>selected);
|
||||
}
|
||||
} finally {
|
||||
popover_open = true;
|
||||
setTimeout(() => {
|
||||
popover_open = false;
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const on_parent_click = (e: Event) => {
|
||||
if (e.target !== e.currentTarget) {
|
||||
return;
|
||||
}
|
||||
open = false;
|
||||
};
|
||||
const close_start_game_if_esc_is_pressed = (key: KeyboardEvent) => {
|
||||
if (key.code === 'Escape') {
|
||||
open = false;
|
||||
}
|
||||
};
|
||||
onMount(() => {
|
||||
document.body.addEventListener('keydown', close_start_game_if_esc_is_pressed);
|
||||
});
|
||||
</script>
|
||||
|
||||
<SmallPopover bind:open={popover_open} type={PopoverTypes.Copy} />
|
||||
<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 gap-2 overflow-scroll"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<BrownButton>{$t('quiztivity.editor.shares.add_new_share')}</BrownButton>
|
||||
</div>
|
||||
{#await load_shares()}
|
||||
<Spinner />
|
||||
{:then shares}
|
||||
{#each shares as share}
|
||||
<div class="grid grid-cols-4 w-full gap-2">
|
||||
<!-- <p>{share.name ?? "..."}</p>-->
|
||||
<div class="w-full mx-auto">
|
||||
<BrownButton
|
||||
flex={true}
|
||||
disabled={browser
|
||||
? !navigator.canShare({
|
||||
title: 'title',
|
||||
url: `${window.location.origin}/quiztivity`
|
||||
})
|
||||
: false}
|
||||
on:click={() => {
|
||||
console.log(
|
||||
navigator.canShare({
|
||||
title: 'title',
|
||||
url: `${window.location.origin}/quiztivity`
|
||||
})
|
||||
);
|
||||
navigator.share({
|
||||
title: 'Quiztivity on ClassQuiz',
|
||||
text: 'Play this Quiztivity now on ClassQuiz!',
|
||||
url: `${window.location.origin}/quiztivity/share/${share.id}?ref=share`
|
||||
});
|
||||
}}
|
||||
>
|
||||
<!-- heroicons/share -->
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</BrownButton>
|
||||
</div>
|
||||
<div class="w-full mx-auto">
|
||||
<BrownButton
|
||||
flex={true}
|
||||
on:click={() => {
|
||||
copyToClipboard(
|
||||
`${window.location.origin}/quiztivity/share/${share.id}?ref=copy`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<!-- heroicons/ClipboardCopy -->
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</BrownButton>
|
||||
</div>
|
||||
<p class="m-auto">
|
||||
{#if share.expire_in}
|
||||
{$t('quiztivity.editor.shares.expires_on', {
|
||||
date: DateTime.now()
|
||||
.plus({ minutes: share.expire_in })
|
||||
.toJSDate()
|
||||
.toLocaleString()
|
||||
})}
|
||||
{:else}
|
||||
{$t('quiztivity.editor.shares.never_expires')}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="w-fit my-auto ml-auto">
|
||||
<BrownButton>{$t('words.delete')}</BrownButton>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,8 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import Editor from '../../../lib/quiztivity/editor.svelte';
|
||||
import Editor from '$lib/quiztivity/editor.svelte';
|
||||
import SharesPopover from '$lib/quiztivity/shares_popover.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data: PageData;
|
||||
@@ -13,6 +14,7 @@
|
||||
let saving = false;
|
||||
|
||||
let quiztivity = data.quiztivity;
|
||||
let shares_menu_open = false;
|
||||
|
||||
const save_quiztivity = async () => {
|
||||
saving = true;
|
||||
@@ -37,3 +39,7 @@
|
||||
<div class="h-full">
|
||||
<Editor bind:data={quiztivity} on:save={save_quiztivity} bind:saving />
|
||||
</div>
|
||||
|
||||
{#if shares_menu_open}
|
||||
<SharesPopover id={quiztivity.id} bind:open={shares_menu_open} />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Added quiztivityshares
|
||||
|
||||
Revision ID: 8ac2bed1718e
|
||||
Revises: 4bbe1850b61a
|
||||
Create Date: 2023-05-14 12:04:03.639173
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import ormar
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "8ac2bed1718e"
|
||||
down_revision = "4bbe1850b61a"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"quiztivityshares",
|
||||
sa.Column("id", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=False),
|
||||
sa.Column("name", sa.Text(), nullable=True),
|
||||
sa.Column("expire_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("quiztivity", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True),
|
||||
sa.Column("user", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["quiztivity"], ["quiztivitys.id"], name="fk_quiztivityshares_quiztivitys_id_quiztivity"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_quiztivityshares_users_id_user"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("quiztivityshares")
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user