Add pool input box in post details

This commit is contained in:
Ruin0x11 2020-05-04 02:20:23 -07:00
parent e6bf102bc0
commit 8795279a73
11 changed files with 337 additions and 6 deletions

View file

@ -0,0 +1,8 @@
<div class='pool-input'>
<div class='main-control'>
<input type='text' placeholder='type to add…'/>
<!-- <button>Add</button> -->
</div>
<ul class='compact-pools'></ul>
</div>

View file

@ -87,6 +87,12 @@
</section> </section>
<% } %> <% } %>
<% if (ctx.canEditPoolPosts) { %>
<section class='pools'>
<%= ctx.makeTextInput({}) %>
</section>
<% } %>
<% if (ctx.canEditPostContent) { %> <% if (ctx.canEditPostContent) { %>
<section class='post-content'> <section class='post-content'>
<label>Content</label> <label>Content</label>

View file

@ -0,0 +1,230 @@
'use strict';
const api = require('../api.js');
const pools = require('../pools.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const Pool = require('../models/pool.js');
const settings = require('../models/settings.js');
const events = require('../events.js');
const views = require('../util/views.js');
const PoolAutoCompleteControl = require('./pool_auto_complete_control.js');
const KEY_SPACE = 32;
const KEY_RETURN = 13;
const SOURCE_INIT = 'init';
const SOURCE_IMPLICATION = 'implication';
const SOURCE_USER_INPUT = 'user-input';
const SOURCE_CLIPBOARD = 'clipboard';
const template = views.getTemplate('pool-input');
function _fadeOutListItemNodeStatus(listItemNode) {
if (listItemNode.classList.length) {
if (listItemNode.fadeTimeout) {
window.clearTimeout(listItemNode.fadeTimeout);
}
listItemNode.fadeTimeout = window.setTimeout(() => {
while (listItemNode.classList.length) {
listItemNode.classList.remove(
listItemNode.classList.item(0));
}
listItemNode.fadeTimeout = null;
}, 2500);
}
}
class PoolInputControl extends events.EventTarget {
constructor(hostNode, poolList) {
super();
this.pools = poolList;
this._hostNode = hostNode;
this._poolToListItemNode = new Map();
// dom
const editAreaNode = template();
this._editAreaNode = editAreaNode;
this._poolInputNode = editAreaNode.querySelector('input');
this._poolListNode = editAreaNode.querySelector('ul.compact-pools');
this._autoCompleteControl = new PoolAutoCompleteControl(
this._poolInputNode, {
getTextToFind: () => {
return this._poolInputNode.value;
},
confirm: pool => {
this._poolInputNode.value = '';
this.addPool(pool, SOURCE_USER_INPUT);
},
delete: pool => {
this._poolInputNode.value = '';
this.deletePool(pool);
},
verticalShift: -2
});
// dom events
this._poolInputNode.addEventListener(
'keydown', e => this._evtInputKeyDown(e));
// show
this._hostNode.style.display = 'none';
this._hostNode.parentNode.insertBefore(
this._editAreaNode, hostNode.nextSibling);
// add existing pools
for (let pool of [...this.pools]) {
const listItemNode = this._createListItemNode(pool);
this._poolListNode.appendChild(listItemNode);
}
}
addPoolByText(text, source) {
for (let poolName of text.split(/\s+/).filter(word => word).reverse()) {
this.addPoolByName(poolName, source);
}
}
addPoolByName(name, source) {
name = name.trim();
if (!name) {
return;
}
return Pool.get(name).then(pool => {
return this.addPool(pool, source);
}, () => {
const pool = new Pool();
pool.names = [name];
pool.category = null;
return this.addPool(pool, source);
});
}
addPool(pool, source) {
if (source != SOURCE_INIT && this.pools.hasPoolId(pool.id)) {
return Promise.resolve();
}
this.pools.add(pool, false)
const listItemNode = this._createListItemNode(pool);
if (!pool.category) {
listItemNode.classList.add('new');
}
this._poolListNode.prependChild(listItemNode);
_fadeOutListItemNodeStatus(listItemNode);
this.dispatchEvent(new CustomEvent('add', {
detail: {pool: pool, source: source},
}));
this.dispatchEvent(new CustomEvent('change'));
return Promise.resolve();
}
deletePool(pool) {
if (!this.pools.hasPoolId(pool.id)) {
return;
}
this.pools.removeById(pool.id);
this._hideAutoComplete();
this._deleteListItemNode(pool);
this.dispatchEvent(new CustomEvent('remove', {
detail: {pool: pool},
}));
this.dispatchEvent(new CustomEvent('change'));
}
_evtAddPoolButtonClick(e) {
// TODO
// e.preventDefault();
// this.addPoolByName(this._poolInputNode.value, SOURCE_USER_INPUT);
// this._poolInputNode.value = '';
}
_evtInputKeyDown(e) {
// TODO
if (e.which == KEY_RETURN || e.which == KEY_SPACE) {
e.preventDefault();
// this._hideAutoComplete();
// this.addPoolByText(this._poolInputNode.value, SOURCE_USER_INPUT);
// this._poolInputNode.value = '';
}
}
_createListItemNode(pool) {
const className = pool.category ?
misc.makeCssName(pool.category, 'pool') :
null;
const poolLinkNode = document.createElement('a');
if (className) {
poolLinkNode.classList.add(className);
}
poolLinkNode.setAttribute(
'href', uri.formatClientLink('pool', pool.names[0]));
const poolIconNode = document.createElement('i');
poolIconNode.classList.add('fa');
poolIconNode.classList.add('fa-pool');
poolLinkNode.appendChild(poolIconNode);
const searchLinkNode = document.createElement('a');
if (className) {
searchLinkNode.classList.add(className);
}
searchLinkNode.setAttribute(
'href', uri.formatClientLink(
'posts', {query: uri.escapeColons(pool.names[0])}));
searchLinkNode.textContent = pool.names[0] + ' ';
searchLinkNode.addEventListener('click', e => {
e.preventDefault();
});
const usagesNode = document.createElement('span');
usagesNode.classList.add('pool-usages');
usagesNode.setAttribute('data-pseudo-content', pool.postCount);
const removalLinkNode = document.createElement('a');
removalLinkNode.classList.add('remove-pool');
removalLinkNode.setAttribute('href', '');
removalLinkNode.setAttribute('data-pseudo-content', '×');
removalLinkNode.addEventListener('click', e => {
e.preventDefault();
this.deletePool(pool);
});
const listItemNode = document.createElement('li');
listItemNode.appendChild(removalLinkNode);
listItemNode.appendChild(poolLinkNode);
listItemNode.appendChild(searchLinkNode);
listItemNode.appendChild(usagesNode);
for (let name of pool.names) {
this._poolToListItemNode.set(name, listItemNode);
}
return listItemNode;
}
_deleteListItemNode(pool) {
const listItemNode = this._getListItemNode(pool);
if (listItemNode) {
listItemNode.parentNode.removeChild(listItemNode);
}
for (let name of pool.names) {
this._poolToListItemNode.delete(name);
}
}
_getListItemNode(pool) {
return this._poolToListItemNode.get(pool.names[0]);
}
_hideAutoComplete() {
this._autoCompleteControl.hide();
}
}
module.exports = PoolInputControl;

View file

@ -7,6 +7,7 @@ const views = require('../util/views.js');
const Note = require('../models/note.js'); const Note = require('../models/note.js');
const Point = require('../models/point.js'); const Point = require('../models/point.js');
const TagInputControl = require('./tag_input_control.js'); const TagInputControl = require('./tag_input_control.js');
const PoolInputControl = require('./pool_input_control.js');
const ExpanderControl = require('../controls/expander_control.js'); const ExpanderControl = require('../controls/expander_control.js');
const FileDropperControl = require('../controls/file_dropper_control.js'); const FileDropperControl = require('../controls/file_dropper_control.js');
@ -37,7 +38,8 @@ class PostEditSidebarControl extends events.EventTarget {
canEditPostFlags: api.hasPrivilege('posts:edit:flags'), canEditPostFlags: api.hasPrivilege('posts:edit:flags'),
canEditPostContent: api.hasPrivilege('posts:edit:content'), canEditPostContent: api.hasPrivilege('posts:edit:content'),
canEditPostThumbnail: api.hasPrivilege('posts:edit:thumbnail'), canEditPostThumbnail: api.hasPrivilege('posts:edit:thumbnail'),
canEditPostSource : api.hasPrivilege('posts:edit:source'), canEditPostSource: api.hasPrivilege('posts:edit:source'),
canEditPoolPosts: api.hasPrivilege('pools:edit:posts'),
canCreateAnonymousPosts: api.hasPrivilege('posts:create:anonymous'), canCreateAnonymousPosts: api.hasPrivilege('posts:create:anonymous'),
canDeletePosts: api.hasPrivilege('posts:delete'), canDeletePosts: api.hasPrivilege('posts:delete'),
canFeaturePosts: api.hasPrivilege('posts:feature'), canFeaturePosts: api.hasPrivilege('posts:feature'),
@ -56,6 +58,10 @@ class PostEditSidebarControl extends events.EventTarget {
'post-notes', 'post-notes',
'Notes', 'Notes',
this._hostNode.querySelectorAll('.notes')); this._hostNode.querySelectorAll('.notes'));
this._poolsExpander = new ExpanderControl(
'post-pools',
`Pools (${this._post.pools.length})`,
this._hostNode.querySelectorAll('.pools'));
new ExpanderControl( new ExpanderControl(
'post-content', 'post-content',
'Content', 'Content',
@ -76,6 +82,11 @@ class PostEditSidebarControl extends events.EventTarget {
this._tagInputNode, post.tags); this._tagInputNode, post.tags);
} }
if (this._poolInputNode) {
this._poolControl = new PoolInputControl(
this._poolInputNode, post.pools);
}
if (this._contentInputNode) { if (this._contentInputNode) {
this._contentFileDropper = new FileDropperControl( this._contentFileDropper = new FileDropperControl(
this._contentInputNode, { this._contentInputNode, {
@ -170,6 +181,9 @@ class PostEditSidebarControl extends events.EventTarget {
this._post.notes.addEventListener(eventType, e => { this._post.notes.addEventListener(eventType, e => {
this._syncExpanderTitles(); this._syncExpanderTitles();
}); });
this._post.pools.addEventListener(eventType, e => {
this._syncExpanderTitles();
});
} }
this._tagControl.addEventListener( this._tagControl.addEventListener(
@ -182,11 +196,18 @@ class PostEditSidebarControl extends events.EventTarget {
this._noteTextareaNode.addEventListener( this._noteTextareaNode.addEventListener(
'change', e => this._evtNoteTextChangeRequest(e)); 'change', e => this._evtNoteTextChangeRequest(e));
} }
this._poolControl.addEventListener(
'change', e => {
this.dispatchEvent(new CustomEvent('change'));
this._syncExpanderTitles();
});
} }
_syncExpanderTitles() { _syncExpanderTitles() {
this._notesExpander.title = `Notes (${this._post.notes.length})`; this._notesExpander.title = `Notes (${this._post.notes.length})`;
this._tagsExpander.title = `Tags (${this._post.tags.length})`; this._tagsExpander.title = `Tags (${this._post.tags.length})`;
this._poolsExpander.title = `Pools (${this._post.pools.length})`;
} }
_evtPostContentChange(e) { _evtPostContentChange(e) {
@ -338,6 +359,10 @@ class PostEditSidebarControl extends events.EventTarget {
misc.splitByWhitespace(this._tagInputNode.value) : misc.splitByWhitespace(this._tagInputNode.value) :
undefined, undefined,
pools: this._poolInputNode ?
misc.splitByWhitespace(this._poolInputNode.value) :
undefined,
relations: this._relationsInputNode ? relations: this._relationsInputNode ?
misc.splitByWhitespace(this._relationsInputNode.value) misc.splitByWhitespace(this._relationsInputNode.value)
.map(x => parseInt(x)) : .map(x => parseInt(x)) :
@ -374,6 +399,10 @@ class PostEditSidebarControl extends events.EventTarget {
return this._formNode.querySelector('.tags input'); return this._formNode.querySelector('.tags input');
} }
get _poolInputNode() {
return this._formNode.querySelector('.pools input');
}
get _loopVideoInputNode() { get _loopVideoInputNode() {
return this._formNode.querySelector('.flags input[name=loop]'); return this._formNode.querySelector('.flags input[name=loop]');
} }

View file

@ -22,6 +22,23 @@ class PoolList extends AbstractList {
{results: PoolList.fromResponse(response.results)})); {results: PoolList.fromResponse(response.results)}));
}); });
} }
hasPoolId(poolId) {
for (let pool of this._list) {
if (pool.id === poolId) {
return true;
}
}
return false;
}
removeById(poolId) {
for (let pool of this._list) {
if (pool.id === poolId) {
this.remove(pool);
}
}
}
} }
PoolList._itemClass = Pool; PoolList._itemClass = Pool;

View file

@ -7,6 +7,7 @@ const events = require('../events.js');
const TagList = require('./tag_list.js'); const TagList = require('./tag_list.js');
const NoteList = require('./note_list.js'); const NoteList = require('./note_list.js');
const CommentList = require('./comment_list.js'); const CommentList = require('./comment_list.js');
const PoolList = require('./pool_list.js');
const misc = require('../util/misc.js'); const misc = require('../util/misc.js');
class Post extends events.EventTarget { class Post extends events.EventTarget {
@ -18,6 +19,7 @@ class Post extends events.EventTarget {
obj._tags = new TagList(); obj._tags = new TagList();
obj._notes = new NoteList(); obj._notes = new NoteList();
obj._comments = new CommentList(); obj._comments = new CommentList();
obj._pools = new PoolList();
} }
this._updateFromResponse({}); this._updateFromResponse({});
@ -46,6 +48,7 @@ class Post extends events.EventTarget {
get notes() { return this._notes; } get notes() { return this._notes; }
get comments() { return this._comments; } get comments() { return this._comments; }
get relations() { return this._relations; } get relations() { return this._relations; }
get pools() { return this._pools; }
get score() { return this._score; } get score() { return this._score; }
get commentCount() { return this._commentCount; } get commentCount() { return this._commentCount; }
@ -128,6 +131,7 @@ class Post extends events.EventTarget {
if (this._source !== this._orig._source) { if (this._source !== this._orig._source) {
detail.source = this._source; detail.source = this._source;
} }
// TODO pools
let apiPromise = this._id ? let apiPromise = this._id ?
api.put(uri.formatApiLink('post', this.id), detail, files) : api.put(uri.formatApiLink('post', this.id), detail, files) :
@ -304,6 +308,7 @@ class Post extends events.EventTarget {
obj._tags.sync(response.tags); obj._tags.sync(response.tags);
obj._notes.sync(response.notes); obj._notes.sync(response.notes);
obj._comments.sync(response.comments); obj._comments.sync(response.comments);
obj._pools.sync(response.pools);
} }
Object.assign(this, map()); Object.assign(this, map());

View file

@ -34,3 +34,22 @@ def _bump_query_count() -> None:
sa.event.listen(_engine, 'after_execute', lambda *args: _bump_query_count()) sa.event.listen(_engine, 'after_execute', lambda *args: _bump_query_count())
import time
import logging
logger = logging.getLogger("myapp.sqltime")
logger.setLevel(logging.INFO)
def before_cursor_execute(conn, cursor, statement,
parameters, context, executemany):
conn.info.setdefault('query_start_time', []).append(time.time())
logger.info("Start Query: %s" % statement)
def after_cursor_execute(conn, cursor, statement,
parameters, context, executemany):
total = time.time() - conn.info['query_start_time'].pop(-1)
logger.info("Total Time: %f" % total)
sa.event.listen(_engine, "before_cursor_execute", before_cursor_execute)
sa.event.listen(_engine, "after_cursor_execute", after_cursor_execute)

View file

@ -5,7 +5,7 @@ from datetime import datetime
import sqlalchemy as sa import sqlalchemy as sa
from szurubooru import config, db, model, errors, rest from szurubooru import config, db, model, errors, rest
from szurubooru.func import ( from szurubooru.func import (
users, scores, comments, tags, util, users, scores, comments, tags, pools, util,
mime, images, files, image_hash, serialization, snapshots) mime, images, files, image_hash, serialization, snapshots)
@ -176,6 +176,7 @@ class PostSerializer(serialization.BaseSerializer):
'hasCustomThumbnail': self.serialize_has_custom_thumbnail, 'hasCustomThumbnail': self.serialize_has_custom_thumbnail,
'notes': self.serialize_notes, 'notes': self.serialize_notes,
'comments': self.serialize_comments, 'comments': self.serialize_comments,
'pools': self.serialize_pools,
} }
def serialize_id(self) -> Any: def serialize_id(self) -> Any:
@ -299,6 +300,14 @@ class PostSerializer(serialization.BaseSerializer):
self.post.comments, self.post.comments,
key=lambda comment: comment.creation_time)] key=lambda comment: comment.creation_time)]
def serialize_pools(self) -> Any:
return [
pools.serialize_pool(pool)
for pool in sorted(
self.post.pools,
key=lambda pool: pool.creation_time)]
def serialize_post( def serialize_post(
post: Optional[model.Post], post: Optional[model.Post],

View file

@ -48,7 +48,7 @@ def upgrade():
op.create_table( op.create_table(
'pool_post', 'pool_post',
sa.Column('pool_id', sa.Integer(), nullable=False), sa.Column('pool_id', sa.Integer(), nullable=False),
sa.Column('post_id', sa.Integer(), nullable=False), sa.Column('post_id', sa.Integer(), nullable=False, index=True),
sa.Column('ord', sa.Integer(), nullable=False, index=True), sa.Column('ord', sa.Integer(), nullable=False, index=True),
sa.ForeignKeyConstraint(['pool_id'], ['pool.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['pool_id'], ['pool.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['post_id'], ['post.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['post_id'], ['post.id'], ondelete='CASCADE'),

View file

@ -2,7 +2,6 @@ import sqlalchemy as sa
from sqlalchemy.ext.orderinglist import ordering_list from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from szurubooru.model.base import Base from szurubooru.model.base import Base
import szurubooru.model as model
class PoolName(Base): class PoolName(Base):
@ -43,9 +42,9 @@ class PoolPost(Base):
order = sa.Column('ord', sa.Integer, nullable=False, index=True) order = sa.Column('ord', sa.Integer, nullable=False, index=True)
pool = sa.orm.relationship('Pool', back_populates='_posts') pool = sa.orm.relationship('Pool', back_populates='_posts')
post = sa.orm.relationship('Post') post = sa.orm.relationship('Post', back_populates='_pools')
def __init__(self, post: model.Post) -> None: def __init__(self, post) -> None:
self.post_id = post.post_id self.post_id = post.post_id
class Pool(Base): class Pool(Base):

View file

@ -2,7 +2,10 @@ from typing import List
import sqlalchemy as sa import sqlalchemy as sa
from szurubooru.model.base import Base from szurubooru.model.base import Base
from szurubooru.model.comment import Comment from szurubooru.model.comment import Comment
from szurubooru.model.pool import PoolPost
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.orderinglist import ordering_list
class PostFeature(Base): class PostFeature(Base):
@ -224,6 +227,12 @@ class Post(Base):
notes = sa.orm.relationship( notes = sa.orm.relationship(
'PostNote', cascade='all, delete-orphan', lazy='joined') 'PostNote', cascade='all, delete-orphan', lazy='joined')
comments = sa.orm.relationship('Comment', cascade='all, delete-orphan') comments = sa.orm.relationship('Comment', cascade='all, delete-orphan')
_pools = sa.orm.relationship(
'PoolPost',
lazy='select',
order_by='PoolPost.order',
back_populates='post')
pools = association_proxy('_pools', 'pool')
# dynamic columns # dynamic columns
tag_count = sa.orm.column_property( tag_count = sa.orm.column_property(