Skip to content

Commit

Permalink
Prepare data layers view UI
Browse files Browse the repository at this point in the history
  • Loading branch information
bkis committed Nov 21, 2023
1 parent 7d88322 commit 3545583
Show file tree
Hide file tree
Showing 15 changed files with 433 additions and 76 deletions.
29 changes: 20 additions & 9 deletions Tekst-API/tekst/models/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Annotated

from beanie import PydanticObjectId
from beanie.operators import Or
from beanie.operators import And, In, Or
from pydantic import ConfigDict, Field, field_validator, model_validator

from tekst.models.common import (
Expand All @@ -12,6 +12,7 @@
ModelBase,
ModelFactoryMixin,
)
from tekst.models.text import TextDocument
from tekst.models.user import UserRead


Expand Down Expand Up @@ -104,18 +105,28 @@ def restricted_fields(self, user_id: str = None) -> dict:

class LayerBaseDocument(LayerBase, DocumentBase):
@classmethod
def allowed_to_read(cls, user: UserRead | None) -> dict:
async def allowed_to_read(cls, user: UserRead | None) -> dict:
if not user:
return {"public": True}
if user.is_superuser:
return {}
uid = user.id if user else "no_id"
return Or(
{"public": True},
{"proposed": True},
{"owner_id": uid},
{"shared_read": uid},
{"shared_write": uid},

active_texts_ids = [
text.id
for text in await TextDocument.find(
TextDocument.is_active == True # noqa: E712
).to_list()
]

return And(
Or(In(LayerBaseDocument.text_id, active_texts_ids), {"owner_id": user.id}),
Or(
{"public": True},
{"proposed": True},
{"owner_id": user.id},
{"shared_read": user.id},
{"shared_write": user.id},
),
)

@classmethod
Expand Down
4 changes: 2 additions & 2 deletions Tekst-API/tekst/routers/browse.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async def get_unit_siblings(

layer = await LayerBaseDocument.find_one(
LayerBaseDocument.id == layer_id,
LayerBaseDocument.allowed_to_read(user),
await LayerBaseDocument.allowed_to_read(user),
with_children=True,
)

Expand Down Expand Up @@ -182,7 +182,7 @@ async def get_layer_coverage_data(
) -> list[LayerNodeCoverage]:
layer_doc = await LayerBaseDocument.find_one(
LayerBaseDocument.id == layer_id,
LayerBaseDocument.allowed_to_read(user),
await LayerBaseDocument.allowed_to_read(user),
with_children=True,
)
if not layer_doc:
Expand Down
34 changes: 9 additions & 25 deletions Tekst-API/tekst/routers/layers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from typing import Annotated

from beanie import PydanticObjectId
from beanie.operators import In
from fastapi import APIRouter, HTTPException, Path, Query, status

from tekst.auth import OptionalUserDep, UserDep
Expand All @@ -19,7 +18,8 @@ async def get_layer(
) -> layer_read_model:
"""A generic route for reading a layer definition from the database"""
layer_doc = await layer_document_model.find(
layer_document_model.id == id, layer_document_model.allowed_to_read(user)
layer_document_model.id == id,
await layer_document_model.allowed_to_read(user),
).first_or_none()
if not layer_doc:
raise HTTPException(
Expand Down Expand Up @@ -205,7 +205,6 @@ async def find_layers(
As the resulting list of data layers may contain layers of different types, the
returned layer objects cannot be typed to their precise layer type.
"""

example = {"text_id": text_id}

# add to example
Expand All @@ -214,23 +213,10 @@ async def find_layers(
if layer_type:
example["layer_type"] = layer_type

active_texts = await TextDocument.find(
TextDocument.is_active == True # noqa: E712
).to_list()

# prepare find query to restrict to layers of active texts
active_texts_restriction = (
In(LayerBaseDocument.text_id, [text.id for text in active_texts])
if not (user and user.is_superuser)
else {}
)

# query for layers the user is allowed to read and that belong to active texts
layer_docs = (
await LayerBaseDocument.find(example, with_children=True)
.find(
LayerBaseDocument.allowed_to_read(user),
active_texts_restriction,
await LayerBaseDocument.find(
example, await LayerBaseDocument.allowed_to_read(user), with_children=True
)
.limit(limit)
.to_list()
Expand Down Expand Up @@ -328,13 +314,11 @@ async def get_generic_layer_data_by_id(
),
] = False,
) -> dict:
layer_doc = (
await LayerBaseDocument.find(
LayerBaseDocument.id == layer_id, with_children=True
)
.find(LayerBaseDocument.allowed_to_read(user))
.first_or_none()
)
layer_doc = await LayerBaseDocument.find(
LayerBaseDocument.id == layer_id,
await LayerBaseDocument.allowed_to_read(user),
with_children=True,
).first_or_none()
if not layer_doc:
raise HTTPException(
status.HTTP_404_NOT_FOUND, detail=f"No layer with ID {layer_id}"
Expand Down
22 changes: 5 additions & 17 deletions Tekst-API/tekst/routers/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from tekst.auth import OptionalUserDep, UserDep
from tekst.layer_types import layer_type_manager
from tekst.models.layer import LayerBaseDocument
from tekst.models.text import TextDocument
from tekst.models.unit import UnitBase, UnitBaseDocument


Expand All @@ -21,10 +20,10 @@ async def get_unit(id: PydanticObjectId, user: OptionalUserDep) -> unit_read_mod
# check if the layer this unit belongs to is readable by user
layer_read_allowed = unit_doc and (
await LayerBaseDocument.find(
LayerBaseDocument.id == unit_doc.layer_id, with_children=True
)
.find(LayerBaseDocument.allowed_to_read(user))
.exists()
LayerBaseDocument.id == unit_doc.layer_id,
await LayerBaseDocument.allowed_to_read(user),
with_children=True,
).exists()
)
unit_doc = unit_doc if layer_read_allowed else None
if not unit_doc:
Expand Down Expand Up @@ -195,19 +194,8 @@ async def find_units(
returned unit objects cannot be typed to their precise layer unit type.
"""

active_texts = await TextDocument.find(
TextDocument.is_active == True # noqa: E712
).to_list()

active_texts_restriction = (
{}
if user and user.is_superuser
else In(LayerBaseDocument.text_id, [text.id for text in active_texts])
)

readable_layers = await LayerBaseDocument.find(
LayerBaseDocument.allowed_to_read(user),
active_texts_restriction,
await LayerBaseDocument.allowed_to_read(user),
with_children=True,
).to_list()

Expand Down
32 changes: 27 additions & 5 deletions Tekst-Web/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const serverUrl: string | undefined = import.meta.env.TEKST_SERVER_URL;
const apiPath: string | undefined = import.meta.env.TEKST_API_PATH;
const apiUrl = (serverUrl && apiPath && serverUrl + apiPath) || '/';

// custom, monkeypatched "fetch" for implementing request/response interceptors
// custom, modified "fetch" for implementing request/response interceptors
const customFetch = async (input: RequestInfo | URL, init?: RequestInit | undefined) => {
// --- request interceptors go here... ---
// add XSRF header to request headers
Expand Down Expand Up @@ -72,25 +72,47 @@ export function getFullUrl(path: string, query?: Record<string, any>): URL {
}

// export components types for use throughout codebase

// general

export type ErrorModel = components['schemas']['ErrorModel'];

// user

export type UserCreate = components['schemas']['UserCreate'];
export type UserRead = components['schemas']['UserRead'];
export type UserUpdate = components['schemas']['UserUpdate'];
export type UserReadPublic = components['schemas']['UserReadPublic'];
export type UserUpdatePublicFields = components['schemas']['UserUpdate']['publicFields'];

// text and text structure

export type TextCreate = components['schemas']['TextCreate'];
export type TextRead = components['schemas']['TextRead'];
export type SubtitleTranslation = components['schemas']['SubtitleTranslation'];
export type StructureLevelTranslation = components['schemas']['StructureLevelTranslation'];
export type NodeRead = components['schemas']['NodeRead'];
export type PlainTextLayerConfig = components['schemas']['PlainTextLayerConfig'];
export type DeepLLinksConfig = components['schemas']['DeepLLinksConfig'];
export type LayerNodeCoverage = components['schemas']['LayerNodeCoverage'];

// platform

export type PlatformStats = components['schemas']['PlatformStats'];
export type PlatformData = components['schemas']['PlatformData'];
export type ErrorModel = components['schemas']['ErrorModel'];
export type PlatformSettingsRead = components['schemas']['PlatformSettingsRead'];
export type PlatformSettingsUpdate = components['schemas']['PlatformSettingsUpdate'];
export type LayerNodeCoverage = components['schemas']['LayerNodeCoverage'];

export type ClientSegmentRead = components['schemas']['ClientSegmentRead'];
export type ClientSegmentCreate = components['schemas']['ClientSegmentCreate'];
export type ClientSegmentUpdate = components['schemas']['ClientSegmentUpdate'];
export type ClientSegmentHead = components['schemas']['ClientSegmentHead'];

// data layers

export type AnyLayerRead = components['schemas']['AnyLayerRead'];
export type AnyLayerReadFull = AnyLayerRead & { writable?: boolean; owner?: UserReadPublic };

export type PlainTextLayerRead = components['schemas']['PlainTextLayerRead'];
export type PlainTextLayerUpdate = components['schemas']['PlainTextLayerUpdate'];

export type PlainTextLayerConfig = components['schemas']['PlainTextLayerConfig'];
export type DeepLLinksConfig = components['schemas']['DeepLLinksConfig'];
5 changes: 5 additions & 0 deletions Tekst-Web/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,8 @@ blockquote {
.n-tree-node-switcher--expanded {
align-self: center;
}

.n-thing-header__title {
font-size: var(--app-ui-font-size) !important;
font-weight: var(--app-ui-font-weight-normal) !important;
}
96 changes: 96 additions & 0 deletions Tekst-Web/src/components/LayerListItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<script setup lang="ts">
import type { AnyLayerReadFull, UserRead } from '@/api';
import { NIcon, NListItem, NThing, NSpace, NButton } from 'naive-ui';
import { computed } from 'vue';
import LayerInfoWidget from '@/components/browse/widgets/LayerInfoWidget.vue';
import LayerPublicationStatus from '@/components/LayerPublicationStatus.vue';
import DeleteFilled from '@vicons/material/DeleteFilled';
import ModeEditFilled from '@vicons/material/ModeEditFilled';
import StarHalfOutlined from '@vicons/material/StarHalfOutlined';
import PublicFilled from '@vicons/material/PublicFilled';
import PublicOffFilled from '@vicons/material/PublicOffFilled';
const props = defineProps<{
targetLayer: AnyLayerReadFull;
currentUser?: UserRead;
}>();
defineEmits(['deleteClick']);
const canDelete = computed(
() =>
props.currentUser &&
(props.currentUser.isSuperuser || props.currentUser.id === props.targetLayer.ownerId)
);
const canPropose = computed(
() =>
props.currentUser &&
(props.currentUser.isSuperuser || props.currentUser.id === props.targetLayer.ownerId) &&
!props.targetLayer.public &&
!props.targetLayer.proposed
);
</script>

<template>
<n-list-item>
<n-thing :title="targetLayer.title" content-style="margin-top: 8px">
<template #description>
<div style="opacity: 0.75; font-size: var(--app-ui-font-size-small)">
<div v-if="targetLayer.description">
{{ targetLayer.description }}
</div>
<div v-if="targetLayer.comment">
{{ $t('models.layer.comment') }}: {{ targetLayer.comment }}
</div>
</div>
</template>
<template #header-extra>
<n-space>
<!-- propose -->
<n-button v-if="canPropose" secondary :title="$t('dataLayers.proposeAction')">
<template #icon>
<n-icon :component="StarHalfOutlined" />
</template>
</n-button>
<!-- make public -->
<n-button
v-if="currentUser?.isSuperuser && targetLayer.proposed"
secondary
:title="$t('dataLayers.makePublicAction')"
>
<template #icon>
<n-icon :component="PublicFilled" />
</template>
</n-button>
<!-- make private -->
<n-button
v-if="currentUser?.isSuperuser && targetLayer.public"
secondary
:title="$t('dataLayers.makePrivateAction')"
>
<template #icon>
<n-icon :component="PublicOffFilled" />
</template>
</n-button>
<!-- edit -->
<n-button v-if="targetLayer.writable" secondary :title="$t('general.editAction')">
<template #icon>
<n-icon :component="ModeEditFilled" />
</template>
</n-button>
<!-- delete -->
<n-button v-if="canDelete" secondary :title="$t('general.deleteAction')">
<template #icon>
<n-icon :component="DeleteFilled" />
</template>
</n-button>
<!-- layer info -->
<LayerInfoWidget :layer="targetLayer" />
</n-space>
</template>
<LayerPublicationStatus :layer="targetLayer" />
</n-thing>
</n-list-item>
</template>
34 changes: 34 additions & 0 deletions Tekst-Web/src/components/LayerPublicationStatus.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup lang="ts">
import type { AnyLayerRead } from '@/api';
import { NIcon } from 'naive-ui';
import { PublicFilled, StarHalfOutlined, PublicOffFilled } from '@vicons/material';
defineProps<{
layer: AnyLayerRead;
}>();
</script>

<template>
<div style="font-size: var(--app-ui-font-size-small)">
<div v-if="layer.public" class="layer-publication-status">
<n-icon :component="PublicFilled" style="margin-right: 4px" />{{ $t('dataLayers.public') }}
</div>
<div v-else-if="layer.proposed" class="layer-publication-status">
<n-icon :component="StarHalfOutlined" style="margin-right: 4px" />{{
$t('dataLayers.proposed')
}}
</div>
<div v-else class="layer-publication-status">
<n-icon :component="PublicOffFilled" style="margin-right: 4px" />{{
$t('dataLayers.notPublic')
}}
</div>
</div>
</template>

<style scoped>
.layer-publication-status {
display: flex;
align-items: center;
}
</style>
Loading

0 comments on commit 3545583

Please sign in to comment.