diff --git a/.gitignore b/.gitignore index b21e3adf..b0baec20 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ server/**/lib/ server/**/bin/ server/**/pyvenv.cfg __pycache__/ + +tmp \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..e69de29b diff --git a/client/Dockerfile b/client/Dockerfile index ea5151fa..701e39f0 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM node:lts as builder +FROM --platform=$BUILDPLATFORM node:lts AS builder WORKDIR /opt/app COPY package.json package-lock.json ./ @@ -11,7 +11,7 @@ ARG CLIENT_BUILD_ARGS="" RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS} -FROM --platform=$BUILDPLATFORM scratch as approot +FROM --platform=$BUILDPLATFORM scratch AS approot COPY docker-start.sh / @@ -22,7 +22,7 @@ WORKDIR /var/www COPY --from=builder /opt/app/public/ . -FROM nginx:alpine as release +FROM nginx:alpine AS release RUN apk --no-cache add dumb-init COPY --from=approot / / diff --git a/client/html/post_edit_sidebar.tpl b/client/html/post_edit_sidebar.tpl index 07dcf6f8..8a0fab69 100644 --- a/client/html/post_edit_sidebar.tpl +++ b/client/html/post_edit_sidebar.tpl @@ -4,6 +4,24 @@
+
+ <%= ctx.makeTextInput({ + text: 'Title', + name: 'title', + placeholder: '', + value: ctx.post.title, + }) %> +
+ +
+ <%= ctx.makeTextInput({ + text: 'Description', + name: 'description', + placeholder: '', + value: ctx.post.description, + }) %> +
+ <% if (ctx.enableSafety && ctx.canEditPostSafety) { %>
diff --git a/client/html/post_readonly_sidebar.tpl b/client/html/post_readonly_sidebar.tpl index 47094521..6dee5560 100644 --- a/client/html/post_readonly_sidebar.tpl +++ b/client/html/post_readonly_sidebar.tpl @@ -1,4 +1,5 @@
+

<%= ctx.post.title %>

diff --git a/client/html/posts_page.tpl b/client/html/posts_page.tpl index 52011ad1..be30d4f8 100644 --- a/client/html/posts_page.tpl +++ b/client/html/posts_page.tpl @@ -4,9 +4,20 @@ <% for (let post of ctx.response.results) { %>
  • ' + title='<%- post.title %> (<%- post.type %>) Tags: <%- post.tags.map(tag => '#' + tag.names[0]).join(' ') || 'none' %>' href='<%= ctx.canViewPosts ? ctx.getPostUrl(post.id, ctx.parameters) : '' %>'> <%= ctx.makeThumbnail(post.thumbnailUrl) %> + <%= post.title %> <% if (post.type == 'video' || post.type == 'flash' || post.type == 'animation') { %> diff --git a/client/js/controllers/post_list_controller.js b/client/js/controllers/post_list_controller.js index fdb7b844..5b1b0f4c 100644 --- a/client/js/controllers/post_list_controller.js +++ b/client/js/controllers/post_list_controller.js @@ -14,6 +14,7 @@ const EmptyView = require("../views/empty_view.js"); const fields = [ "id", "thumbnailUrl", + "title", "type", "safety", "score", diff --git a/client/js/controllers/post_main_controller.js b/client/js/controllers/post_main_controller.js index bd338129..a2ee5199 100644 --- a/client/js/controllers/post_main_controller.js +++ b/client/js/controllers/post_main_controller.js @@ -169,6 +169,12 @@ class PostMainController extends BasePostController { this._view.sidebarControl.disableForm(); this._view.sidebarControl.clearMessages(); const post = e.detail.post; + if (e.detail.title !== undefined && e.detail.title !== null) { + post.title = e.detail.title; + } + if (e.detail.description !== undefined && e.detail.description !== null) { + post.description = e.detail.description; + } if (e.detail.safety !== undefined && e.detail.safety !== null) { post.safety = e.detail.safety; } diff --git a/client/js/controls/post_edit_sidebar_control.js b/client/js/controls/post_edit_sidebar_control.js index 3b1c16e7..67c4336d 100644 --- a/client/js/controls/post_edit_sidebar_control.js +++ b/client/js/controls/post_edit_sidebar_control.js @@ -55,7 +55,7 @@ class PostEditSidebarControl extends events.EventTarget { "post-info", "Basic info", this._hostNode.querySelectorAll( - ".safety, .relations, .flags, .post-source" + ".title, .description, .safety, .relations, .flags, .post-source" ) ); this._tagsExpander = new ExpanderControl( @@ -398,6 +398,10 @@ class PostEditSidebarControl extends events.EventTarget { detail: { post: this._post, + title: this._titleInputNode.value, + + description: this._descriptionInputNode.value, + safety: this._safetyButtonNodes.length ? Array.from(this._safetyButtonNodes) .filter((node) => node.checked)[0] @@ -481,6 +485,14 @@ class PostEditSidebarControl extends events.EventTarget { return ret; } + get _titleInputNode() { + return this._formNode.querySelector(".title input"); + } + + get _descriptionInputNode() { + return this._formNode.querySelector(".description input"); + } + get _relationsInputNode() { return this._formNode.querySelector(".relations input"); } diff --git a/client/js/models/post.js b/client/js/models/post.js index 01f81bf1..5a996491 100644 --- a/client/js/models/post.js +++ b/client/js/models/post.js @@ -54,6 +54,14 @@ class Post extends events.EventTarget { return this._user; } + get title() { + return this._title; + } + + get description() { + return this._description; + } + get safety() { return this._safety; } @@ -154,6 +162,14 @@ class Post extends events.EventTarget { this._flags = value; } + set title(value) { + this._title = value; + } + + set description(value) { + this._description = value; + } + set safety(value) { this._safety = value; } @@ -250,6 +266,12 @@ class Post extends events.EventTarget { if (anonymous === true) { detail.anonymous = true; } + if (this._title !== this._orig._title) { + detail.title = this._title; + } + if (this._description !== this._orig._description) { + detail.description = this._description; + } if (this._safety !== this._orig._safety) { detail.safety = this._safety; } @@ -471,6 +493,8 @@ class Post extends events.EventTarget { _checksumMD5: response.checksumMD5, _creationTime: response.creationTime, _user: response.user, + _title: response.title, + _description: response.description, _safety: response.safety, _contentUrl: response.contentUrl, _fullContentUrl: new URL( diff --git a/doc/developer-utils/create-alembic-migration.sh b/doc/developer-utils/create-alembic-migration.sh index df7a29eb..9bb26399 100755 --- a/doc/developer-utils/create-alembic-migration.sh +++ b/doc/developer-utils/create-alembic-migration.sh @@ -10,15 +10,16 @@ fi # Create a dummy container WORKDIR="$(git rev-parse --show-toplevel)/server" IMAGE=$(docker build -q "${WORKDIR}") -CONTAINER=$(docker run -d ${IMAGE} tail -f /dev/null) +CONTAINER=$(docker run --network=host -d ${IMAGE} tail -f /dev/null) # Create the migration script docker exec -i \ -e PYTHONPATH='/opt/app' \ - -e POSTGRES_HOST='x' \ - -e POSTGRES_USER='x' \ - -e POSTGRES_PASSWORD='x' \ - ${CONTAINER} alembic revision -m "$1" + -e POSTGRES_HOST='localhost' \ + -e POSTGRES_PORT='15432' \ + -e POSTGRES_USER='szuru' \ + -e POSTGRES_PASSWORD='changeme' \ + ${CONTAINER} alembic revision --autogenerate -m "$1" # Copy the file over from the container docker cp ${CONTAINER}:/opt/app/szurubooru/migrations/versions/ \ diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 00000000..9c26f002 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,48 @@ +## Example Docker Compose configuration +## +## Use this as a template to set up docker-compose, or as guide to set up other +## orchestration services +version: '2' + +services: + + server: + image: szurubooru/server:latest + depends_on: + - sql + environment: + ## These should be the names of the dependent containers listed below, + ## or FQDNs/IP addresses if these services are running outside of Docker + POSTGRES_HOST: sql + ## Credentials for database: + POSTGRES_USER: + POSTGRES_PASSWORD: + ## Commented Values are Default: + #POSTGRES_DB: defaults to same as POSTGRES_USER + #POSTGRES_PORT: 5432 + #LOG_SQL: 0 (1 for verbose SQL logs) + THREADS: + volumes: + - "${MOUNT_DATA}:/data" + - "./server/config.yaml:/opt/app/config.yaml" + + client: + image: szurubooru/client:latest + depends_on: + - server + environment: + BACKEND_HOST: server + BASE_URL: + volumes: + - "${MOUNT_DATA}:/data:ro" + ports: + - "${PORT}:80" + + sql: + image: postgres:11-alpine + restart: unless-stopped + environment: + POSTGRES_USER: szuru + POSTGRES_PASSWORD: changeme + volumes: + - "${MOUNT_SQL}:/var/lib/postgresql/data" diff --git a/docker-compose.yml b/docker-compose.yml index 38e08b97..0d463884 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,6 @@ ## ## Use this as a template to set up docker-compose, or as guide to set up other ## orchestration services -version: '2' - services: server: @@ -15,19 +13,25 @@ services: ## or FQDNs/IP addresses if these services are running outside of Docker POSTGRES_HOST: sql ## Credentials for database: - POSTGRES_USER: - POSTGRES_PASSWORD: + POSTGRES_USER: szuru + POSTGRES_PASSWORD: changeme ## Commented Values are Default: #POSTGRES_DB: defaults to same as POSTGRES_USER #POSTGRES_PORT: 5432 #LOG_SQL: 0 (1 for verbose SQL logs) THREADS: + UID: ${UID} + GID: ${GID} + user: "${UID}:${GID}" volumes: + - "./server:/opt/app" - "${MOUNT_DATA}:/data" - "./server/config.yaml:/opt/app/config.yaml" client: - image: szurubooru/client:latest + # image: szurubooru/client:latest + build: + context: ./client depends_on: - server environment: @@ -44,5 +48,7 @@ services: environment: POSTGRES_USER: POSTGRES_PASSWORD: + ports: + - 15432:5432 volumes: - "${MOUNT_SQL}:/var/lib/postgresql/data" diff --git a/rebuild-and-run.sh b/rebuild-and-run.sh new file mode 100755 index 00000000..2361a76b --- /dev/null +++ b/rebuild-and-run.sh @@ -0,0 +1,3 @@ +docker compose down --volumes +docker compose build client +docker compose up -d \ No newline at end of file diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py index daba7f7e..f4dae1e5 100644 --- a/server/szurubooru/api/post_api.py +++ b/server/szurubooru/api/post_api.py @@ -65,6 +65,14 @@ def create_post( ctx.user, "uploads:use_downloader" ), ) + title = "" + if ctx.has_param("title"): + title = ctx.get_param_as_string("title") + + description = "" + if ctx.has_param("description"): + description = ctx.get_param_as_string("description") + tag_names = ctx.get_param_as_string_list("tags", default=[]) safety = ctx.get_param_as_string("safety") source = ctx.get_param_as_string("source", default="") @@ -81,6 +89,8 @@ def create_post( ) if len(new_tags): auth.verify_privilege(ctx.user, "tags:create") + posts.update_post_title(post, title) + posts.update_post_description(post, description) posts.update_post_safety(post, safety) posts.update_post_source(post, source) posts.update_post_relations(post, relations) @@ -143,6 +153,14 @@ def update_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response: db.session.flush() for tag in new_tags: snapshots.create(tag, ctx.user) + if ctx.has_param("title"): + # for now we're just treating this fields as "content" + auth.verify_privilege(ctx.user, "posts:edit:content") + posts.update_post_title(post, ctx.get_param_as_string("title")) + if ctx.has_param("description"): + # for now we're just treating this fields as "content" + auth.verify_privilege(ctx.user, "posts:edit:content") + posts.update_post_description(post, ctx.get_param_as_string("description")) if ctx.has_param("safety"): auth.verify_privilege(ctx.user, "posts:edit:safety") posts.update_post_safety(post, ctx.get_param_as_string("safety")) diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index be2259cf..28cb941b 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -169,6 +169,8 @@ class PostSerializer(serialization.BaseSerializer): "version": self.serialize_version, "creationTime": self.serialize_creation_time, "lastEditTime": self.serialize_last_edit_time, + "title": self.serialize_title, + "description": self.serialize_description, "safety": self.serialize_safety, "source": self.serialize_source, "type": self.serialize_type, @@ -213,6 +215,12 @@ class PostSerializer(serialization.BaseSerializer): def serialize_last_edit_time(self) -> Any: return self.post.last_edit_time + def serialize_title(self) -> Any: + return self.post.title + + def serialize_description(self) -> Any: + return self.post.description + def serialize_safety(self) -> Any: return SAFETY_MAP[self.post.safety] @@ -414,6 +422,8 @@ def create_post( post.creation_time = datetime.utcnow() post.flags = [] + post.title = "" + post.description = "" post.type = "" post.checksum = "" post.mime_type = "" @@ -441,6 +451,18 @@ def update_post_source(post: model.Post, source: Optional[str]) -> None: raise InvalidPostSourceError("Source is too long.") post.source = source or None +def update_post_title(post: model.Post, title: str) -> None: + assert post + if util.value_exceeds_column_size(title, model.Post.title): + raise InvalidPostSourceError("Title is too long.") + post.title = title + +def update_post_description(post: model.Post, description: str) -> None: + assert post + if util.value_exceeds_column_size(description, model.Post.description): + raise InvalidPostSourceError("Description is too long.") + post.description = description + @sa.events.event.listens_for(model.Post, "after_insert") def _after_post_insert( diff --git a/server/szurubooru/migrations/versions/0c149800a728_add_title_add_desc_to_posts.py b/server/szurubooru/migrations/versions/0c149800a728_add_title_add_desc_to_posts.py new file mode 100644 index 00000000..f34f20f9 --- /dev/null +++ b/server/szurubooru/migrations/versions/0c149800a728_add_title_add_desc_to_posts.py @@ -0,0 +1,24 @@ +''' +Add title and description to posts + +Revision ID: 0c149800a728 +Created at: 2024-12-03 08:21:21.113161 +''' + +import sqlalchemy as sa +from alembic import op + + + +revision = '0c149800a728' +down_revision = 'adcd63ff76a2' +branch_labels = None +depends_on = None + +def upgrade(): + op.add_column('post', sa.Column('title', sa.Unicode(length=512), nullable=False)) + op.add_column('post', sa.Column('description', sa.Unicode(length=2048), nullable=False)) + +def downgrade(): + op.drop_column('post', 'description') + op.drop_column('post', 'title') diff --git a/server/szurubooru/model/post.py b/server/szurubooru/model/post.py index 49e748dc..da621661 100644 --- a/server/szurubooru/model/post.py +++ b/server/szurubooru/model/post.py @@ -215,6 +215,8 @@ class Post(Base): flags_string = sa.Column("flags", sa.Unicode(32), default="") # content description + title = sa.Column("title", sa.Unicode(512), nullable=False, default="") + description = sa.Column("description", sa.Unicode(2048), nullable=False, default="") type = sa.Column("type", sa.Unicode(32), nullable=False) checksum = sa.Column("checksum", sa.Unicode(64), nullable=False) checksum_md5 = sa.Column("checksum_md5", sa.Unicode(32))