Skip to content

Commit

Permalink
Add folder, image for all indices and client_data for branding
Browse files Browse the repository at this point in the history
  • Loading branch information
vanatteveldt committed Feb 9, 2025
1 parent d93b3fc commit bcf8a26
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 35 deletions.
26 changes: 19 additions & 7 deletions amcat4/api/index.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"""API Endpoints for document and index management."""

from datetime import datetime
from http import HTTPStatus
from typing import Annotated, Any, Literal

import elasticsearch
from elastic_transport import ApiError
from fastapi import APIRouter, HTTPException, Response, status, Depends, Body
from fastapi import APIRouter, Body, Depends, HTTPException, Response, status
from pydantic import BaseModel
from datetime import datetime

from amcat4 import index, fields as index_fields
from amcat4 import fields as index_fields
from amcat4 import index
from amcat4.api.auth import authenticated_user, authenticated_writer, check_role

from amcat4.fields import field_stats, field_values
from amcat4.index import refresh_system_index, remove_role, set_role
from amcat4.fields import field_values, field_stats
from amcat4.models import CreateField, FieldType, UpdateField

app_index = APIRouter(prefix="/index", tags=["index"])
Expand Down Expand Up @@ -52,6 +52,8 @@ class NewIndex(BaseModel):
name: str | None = None
guest_role: RoleType | None = None
description: str | None = None
folder: str | None = None
image_url: str | None = None


@app_index.post("/", status_code=status.HTTP_201_CREATED)
Expand All @@ -69,6 +71,8 @@ def create_index(new_index: NewIndex, current_user: str = Depends(authenticated_
name=new_index.name,
description=new_index.description,
admin=current_user if current_user != "_admin" else None,
folder=new_index.folder,
image_url=new_index.image_url,
)
except ApiError as e:
raise HTTPException(
Expand All @@ -85,6 +89,8 @@ class ChangeIndex(BaseModel):
description: str | None = None
guest_role: Literal["WRITER", "READER", "METAREADER", "NONE"] | None = None
archive: bool | None = None
folder: str | None = None
image_url: str | None = None


@app_index.put("/{ix}")
Expand All @@ -111,6 +117,8 @@ def modify_index(ix: str, data: ChangeIndex, user: str = Depends(authenticated_u
description=data.description,
guest_role=guest_role,
archived=archived,
folder=data.folder,
image_url=data.image_url,
# remove_guest_role=remove_guest_role,
# unarchive=unarchive,
)
Expand All @@ -129,6 +137,8 @@ def view_index(ix: str, user: str = Depends(authenticated_user)):
d["guest_role"] = index.GuestRole(d.get("guest_role", 0)).name
d["description"] = d.get("description", "") or ""
d["name"] = d.get("name", "") or ""
d["folder"] = d.get("folder", "") or ""
d["image_url"] = d.get("image_url")
return d
except index.IndexDoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Index {ix} does not exist")
Expand All @@ -140,8 +150,10 @@ def archive_index(
archived: Annotated[bool, Body(description="Boolean for setting archived to true or false")],
user: str = Depends(authenticated_user),
):
"""Archive or unarchive the index. When an index is archived, it restricts usage, and adds a timestamp for when
it was archived. An index can only be deleted if it has been archived for a specific amount of time."""
"""
Archive or unarchive the index. When an index is archived, it restricts usage, and adds a timestamp for when
it was archived.
"""
check_role(user, index.Role.ADMIN, ix)
try:
d = index.get_index(ix)
Expand Down
21 changes: 14 additions & 7 deletions amcat4/api/info.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from fastapi import Depends, Request
from fastapi import APIRouter
from fastapi.templating import Jinja2Templates
from importlib.metadata import version

from fastapi import APIRouter, Depends, Request
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel

from amcat4 import elastic
Expand Down Expand Up @@ -51,11 +50,19 @@ def read_branding():


class ChangeBranding(BaseModel):
server_name: str
server_icon: str
welcome_text: str
server_name: str | None = None
server_icon: str | None = None
server_url: str | None = None
welcome_text: str | None = None
client_data: str | None = None


@app_info.put("/config/branding")
def change_branding(data: ChangeBranding, user: str = Depends(authenticated_admin)):
set_branding(server_icon=data.server_icon, server_name=data.server_name, welcome_text=data.welcome_text)
set_branding(
server_icon=data.server_icon,
server_name=data.server_name,
welcome_text=data.welcome_text,
client_data=data.client_data,
server_url=data.server_url,
)
20 changes: 17 additions & 3 deletions amcat4/elastic.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@
"""

import functools

import logging
from typing import Optional

from elasticsearch import Elasticsearch, NotFoundError

from amcat4.config import get_settings

SYSTEM_INDEX_VERSION = 1
SYSTEM_INDEX_VERSION = 2

SYSTEM_MAPPING = {
"name": {"type": "text"},
"description": {"type": "text"},
"roles": {"type": "nested"},
"summary_field": {"type": "keyword"},
"guest_role": {"type": "keyword"},
"folder": {"type": "keyword"},
"image_url": {"type": "keyword"},
"branding": {"type": "object"},
"external_url": {"type": "keyword"},
}


Expand Down Expand Up @@ -95,11 +99,21 @@ def _setup_elastic():
raise CannotConnectElastic(f"Cannot connect to elasticsearch server {settings.elastic_host}")
if elastic.indices.exists(index=settings.system_index):
# Check index format version
if get_system_version(elastic) is None:
if version := get_system_version(elastic) is None:
raise CannotConnectElastic(
f"System index {settings.elastic_host}::{settings.system_index} is corrupted or uses an "
f"old format. Please repair or migrate before continuing"
)
if version < SYSTEM_INDEX_VERSION:
# Try to set mapping of each field, warn if not possible
for field, fieldtype in SYSTEM_MAPPING.items():
try:
elastic.indices.put_mapping(
index=settings.system_index,
properties={field: fieldtype},
)
except Exception as e:
logging.warning(e)

else:
logging.info(f"Creating amcat4 system index: {settings.system_index}")
Expand Down
57 changes: 39 additions & 18 deletions amcat4/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,20 @@
"""

import collections
from enum import IntEnum
import logging
from multiprocessing import Value
from typing import Any, Iterable, Mapping, Optional, Literal

import hashlib
import json
import logging
from enum import IntEnum
from multiprocessing import Value
from typing import Any, Iterable, Literal, Mapping, Optional

import elasticsearch.helpers
from elasticsearch import NotFoundError

# from amcat4.api.common import py2dict
from amcat4.config import AuthOptions, get_settings
from amcat4.elastic import es
from amcat4.fields import (
coerce_type,
create_fields,
create_or_verify_tag_field,
get_fields,
)
from amcat4.fields import coerce_type, create_fields, create_or_verify_tag_field, get_fields
from amcat4.models import CreateField, Field, FieldType


Expand All @@ -77,7 +71,7 @@ class GuestRole(IntEnum):

Index = collections.namedtuple(
"Index",
["id", "name", "description", "guest_role", "archived", "roles", "summary_field"],
["id", "name", "description", "guest_role", "archived", "roles", "summary_field", "folder", "image_url"],
)


Expand Down Expand Up @@ -131,6 +125,8 @@ def _index_from_elastic(index):
archived=src.get("archived"),
roles=_roles_from_elastic(src.get("roles", [])),
summary_field=src.get("summary_field"),
folder=src.get("folder"),
image_url=src.get("image_url"),
)


Expand All @@ -148,6 +144,8 @@ def create_index(
name: Optional[str] = None,
description: Optional[str] = None,
admin: Optional[str] = None,
folder: Optional[str] = None,
image_url: Optional[str] = None,
) -> None:
"""
Create a new index in elasticsearch and register it with this AmCAT instance
Expand All @@ -165,6 +163,8 @@ def create_index(
name=name or index,
description=description or "",
admin=admin,
folder=folder,
image_url=image_url,
)


Expand All @@ -174,6 +174,8 @@ def register_index(
name: Optional[str] = None,
description: Optional[str] = None,
admin: Optional[str] = None,
folder: Optional[str] = None,
image_url: Optional[str] = None,
) -> None:
"""
Register an existing elastic index with this AmCAT instance, i.e. create an entry in the system index
Expand All @@ -193,6 +195,8 @@ def register_index(
roles=roles,
description=description,
guest_role=guest_role.name if guest_role is not None else "NONE",
folder=folder,
image_url=image_url,
),
)
refresh_index(system_index)
Expand Down Expand Up @@ -279,12 +283,16 @@ def modify_index(
description: Optional[str] = None,
guest_role: Optional[GuestRole] = None,
archived: Optional[str] = None,
folder: Optional[str] = None,
image_url: Optional[str] = None,
):
doc = dict(
name=name,
description=description,
guest_role=guest_role.name if guest_role is not None else None,
archived=archived,
folder=folder,
image_url=image_url,
)

doc = {x: v for (x, v) in doc.items() if v is not None}
Expand Down Expand Up @@ -574,29 +582,42 @@ def update_documents_by_query(index: str | list[str], query: dict, field: str, v


def get_branding():
# We (ab)use the _global settings document for this, even if using summary_field for an icon url is a bit weird
# We (ab)use the _global settings document for this
# (Maybe we should just add a nested object for more flexibility?)
doc = es().get(
index=get_settings().system_index, id=GLOBAL_ROLES, source_includes=["name", "description", "summary_field"]
index=get_settings().system_index,
id=GLOBAL_ROLES,
source_includes=["name", "description", "image_url", "client_data", "external_url"],
)
return dict(
server_name=doc["_source"].get("name"),
server_url=doc["_source"].get("external_url"),
welcome_text=doc["_source"].get("description"),
server_icon=doc["_source"].get("summary_field"),
server_icon=doc["_source"].get("image_url"),
client_data=doc["_source"].get("client_data"),
)


def set_branding(server_name: Optional[str] = None, welcome_text: Optional[str] = None, server_icon: Optional[str] = None):
def set_branding(
server_name: Optional[str] = None,
server_url: Optional[str] = None,
welcome_text: Optional[str] = None,
server_icon: Optional[str] = None,
client_data: Optional[str] = None,
):
"""Change the branding info for this server. Set params to None to keep unchanged, or to '' to delete the entry"""
doc = {}
if server_name is not None:
doc["name"] = server_name or None
if server_url is not None:
doc["external_url"] = server_url or None
if welcome_text is not None:
doc["description"] = welcome_text or None
if server_icon is not None:
doc["summary_field"] = server_icon or None
doc["image_url"] = server_icon or None
if client_data is not None:
doc["client_data"] = client_data or None
if not doc:
# Nothing to do!
return

return es().update(index=get_settings().system_index, id=GLOBAL_ROLES, doc=doc)

0 comments on commit bcf8a26

Please sign in to comment.