Merge pull request #1 from nickcoad/feature/add-title-and-description-to-posts
Feature/add title and description to posts
This commit is contained in:
commit
2a7a73a00e
18 changed files with 214 additions and 15 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -13,3 +13,5 @@ server/**/lib/
|
|||
server/**/bin/
|
||||
server/**/pyvenv.cfg
|
||||
__pycache__/
|
||||
|
||||
tmp
|
0
Makefile
Normal file
0
Makefile
Normal file
|
@ -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 / /
|
||||
|
|
|
@ -4,6 +4,24 @@
|
|||
|
||||
<div class='messages'></div>
|
||||
|
||||
<section class='title'>
|
||||
<%= ctx.makeTextInput({
|
||||
text: 'Title',
|
||||
name: 'title',
|
||||
placeholder: '',
|
||||
value: ctx.post.title,
|
||||
}) %>
|
||||
</section>
|
||||
|
||||
<section class='description'>
|
||||
<%= ctx.makeTextInput({
|
||||
text: 'Description',
|
||||
name: 'description',
|
||||
placeholder: '',
|
||||
value: ctx.post.description,
|
||||
}) %>
|
||||
</section>
|
||||
|
||||
<% if (ctx.enableSafety && ctx.canEditPostSafety) { %>
|
||||
<section class='safety'>
|
||||
<label>Safety</label>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<div class='readonly-sidebar'>
|
||||
<h1 style='line-height: 1.2em'><%= ctx.post.title %></h1>
|
||||
<article class='details'>
|
||||
<section class='download'>
|
||||
<a rel='external' href='<%- ctx.post.contentUrl %>'>
|
||||
|
|
|
@ -4,9 +4,20 @@
|
|||
<% for (let post of ctx.response.results) { %>
|
||||
<li data-post-id='<%= post.id %>'>
|
||||
<a class='thumbnail-wrapper <%= post.tags.length > 0 ? "tags" : "no-tags" %>'
|
||||
title='@<%- post.id %> (<%- post.type %>) Tags: <%- post.tags.map(tag => '#' + tag.names[0]).join(' ') || 'none' %>'
|
||||
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) %>
|
||||
<span class="post-title" style="
|
||||
position: absolute;
|
||||
bottom: 2.3em;
|
||||
left: 0;
|
||||
padding: .33em .5em;
|
||||
color: #fff;
|
||||
line-height: 1.2em;
|
||||
margin: 0.5em;
|
||||
font-size: 12px;
|
||||
background: rgba(0,0,0,0.5);
|
||||
"><%= post.title %></span>
|
||||
<span class='type' data-type='<%- post.type %>'>
|
||||
<% if (post.type == 'video' || post.type == 'flash' || post.type == 'animation') { %>
|
||||
<span class='icon'><i class='fa fa-film'></i></span>
|
||||
|
|
|
@ -14,6 +14,7 @@ const EmptyView = require("../views/empty_view.js");
|
|||
const fields = [
|
||||
"id",
|
||||
"thumbnailUrl",
|
||||
"title",
|
||||
"type",
|
||||
"safety",
|
||||
"score",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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/ \
|
||||
|
|
48
docker-compose.local.yml
Normal file
48
docker-compose.local.yml
Normal file
|
@ -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"
|
|
@ -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"
|
||||
|
|
3
rebuild-and-run.sh
Executable file
3
rebuild-and-run.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
docker compose down --volumes
|
||||
docker compose build client
|
||||
docker compose up -d
|
|
@ -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"))
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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')
|
|
@ -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))
|
||||
|
|
Reference in a new issue