client+server/tag-categories: add ordering feature
This commit is contained in:
commit
a896c1a5a7
17 changed files with 113 additions and 14 deletions
|
@ -7,6 +7,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th class='name'>Category name</th>
|
<th class='name'>Category name</th>
|
||||||
<th class='color'>CSS color</th>
|
<th class='color'>CSS color</th>
|
||||||
|
<th class='order'>Order</th>
|
||||||
<th class='usages'>Usages</th>
|
<th class='usages'>Usages</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -21,7 +22,7 @@
|
||||||
|
|
||||||
<div class='messages'></div>
|
<div class='messages'></div>
|
||||||
|
|
||||||
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canDelete) { %>
|
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canEditOrder || ctx.canDelete) { %>
|
||||||
<div class='buttons'>
|
<div class='buttons'>
|
||||||
<input type='submit' class='save' value='Save changes'>
|
<input type='submit' class='save' value='Save changes'>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,6 +17,13 @@
|
||||||
<%- ctx.tagCategory.color %>
|
<%- ctx.tagCategory.color %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
|
<td class='order'>
|
||||||
|
<% if (ctx.canEditOrder) { %>
|
||||||
|
<%= ctx.makeNumericInput({value: ctx.tagCategory.order}) %>
|
||||||
|
<% } else { %>
|
||||||
|
<%- ctx.tagCategory.order %>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
<td class='usages'>
|
<td class='usages'>
|
||||||
<% if (ctx.tagCategory.name) { %>
|
<% if (ctx.tagCategory.name) { %>
|
||||||
<a href='<%- ctx.formatClientLink('tags', {query: 'category:' + ctx.tagCategory.name}) %>'>
|
<a href='<%- ctx.formatClientLink('tags', {query: 'category:' + ctx.tagCategory.name}) %>'>
|
||||||
|
|
|
@ -26,6 +26,7 @@ class TagCategoriesController {
|
||||||
tagCategories: this._tagCategories,
|
tagCategories: this._tagCategories,
|
||||||
canEditName: api.hasPrivilege("tagCategories:edit:name"),
|
canEditName: api.hasPrivilege("tagCategories:edit:name"),
|
||||||
canEditColor: api.hasPrivilege("tagCategories:edit:color"),
|
canEditColor: api.hasPrivilege("tagCategories:edit:color"),
|
||||||
|
canEditOrder: api.hasPrivilege("tagCategories:edit:order"),
|
||||||
canDelete: api.hasPrivilege("tagCategories:delete"),
|
canDelete: api.hasPrivilege("tagCategories:delete"),
|
||||||
canCreate: api.hasPrivilege("tagCategories:create"),
|
canCreate: api.hasPrivilege("tagCategories:create"),
|
||||||
canSetDefault: api.hasPrivilege(
|
canSetDefault: api.hasPrivilege(
|
||||||
|
|
|
@ -9,10 +9,12 @@ class TagCategory extends events.EventTarget {
|
||||||
super();
|
super();
|
||||||
this._name = "";
|
this._name = "";
|
||||||
this._color = "#000000";
|
this._color = "#000000";
|
||||||
|
this._order = 1;
|
||||||
this._tagCount = 0;
|
this._tagCount = 0;
|
||||||
this._isDefault = false;
|
this._isDefault = false;
|
||||||
this._origName = null;
|
this._origName = null;
|
||||||
this._origColor = null;
|
this._origColor = null;
|
||||||
|
this._origOrder = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
|
@ -23,6 +25,10 @@ class TagCategory extends events.EventTarget {
|
||||||
return this._color;
|
return this._color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get order() {
|
||||||
|
return this._order;
|
||||||
|
}
|
||||||
|
|
||||||
get tagCount() {
|
get tagCount() {
|
||||||
return this._tagCount;
|
return this._tagCount;
|
||||||
}
|
}
|
||||||
|
@ -43,6 +49,10 @@ class TagCategory extends events.EventTarget {
|
||||||
this._color = value;
|
this._color = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set order(value) {
|
||||||
|
this._order = value;
|
||||||
|
}
|
||||||
|
|
||||||
static fromResponse(response) {
|
static fromResponse(response) {
|
||||||
const ret = new TagCategory();
|
const ret = new TagCategory();
|
||||||
ret._updateFromResponse(response);
|
ret._updateFromResponse(response);
|
||||||
|
@ -58,6 +68,9 @@ class TagCategory extends events.EventTarget {
|
||||||
if (this.color !== this._origColor) {
|
if (this.color !== this._origColor) {
|
||||||
detail.color = this.color;
|
detail.color = this.color;
|
||||||
}
|
}
|
||||||
|
if (this.order !== this._origOrder) {
|
||||||
|
detail.order = this.order;
|
||||||
|
}
|
||||||
|
|
||||||
if (!Object.keys(detail).length) {
|
if (!Object.keys(detail).length) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
@ -104,10 +117,12 @@ class TagCategory extends events.EventTarget {
|
||||||
this._version = response.version;
|
this._version = response.version;
|
||||||
this._name = response.name;
|
this._name = response.name;
|
||||||
this._color = response.color;
|
this._color = response.color;
|
||||||
|
this._order = response.order;
|
||||||
this._isDefault = response.default;
|
this._isDefault = response.default;
|
||||||
this._tagCount = response.usages;
|
this._tagCount = response.usages;
|
||||||
this._origName = this.name;
|
this._origName = this.name;
|
||||||
this._origColor = this.color;
|
this._origColor = this.color;
|
||||||
|
this._origOrder = this.order;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,6 +100,13 @@ class TagCategoriesView extends events.EventTarget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const orderInput = rowNode.querySelector(".order input");
|
||||||
|
if (orderInput) {
|
||||||
|
orderInput.addEventListener("change", (e) =>
|
||||||
|
this._evtOrderChange(e, rowNode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const removeLinkNode = rowNode.querySelector(".remove a");
|
const removeLinkNode = rowNode.querySelector(".remove a");
|
||||||
if (removeLinkNode) {
|
if (removeLinkNode) {
|
||||||
removeLinkNode.addEventListener("click", (e) =>
|
removeLinkNode.addEventListener("click", (e) =>
|
||||||
|
@ -147,6 +154,10 @@ class TagCategoriesView extends events.EventTarget {
|
||||||
rowNode._tagCategory.color = e.target.value;
|
rowNode._tagCategory.color = e.target.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_evtOrderChange(e, rowNode) {
|
||||||
|
rowNode._tagCategory.order = e.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
_evtDeleteButtonClick(e, rowNode, link) {
|
_evtDeleteButtonClick(e, rowNode, link) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.target.classList.contains("inactive")) {
|
if (e.target.classList.contains("inactive")) {
|
||||||
|
|
12
doc/API.md
12
doc/API.md
|
@ -321,7 +321,8 @@ data.
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
"name": <name>,
|
"name": <name>,
|
||||||
"color": <color>
|
"color": <color>,
|
||||||
|
"order": <order> // optional
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -354,6 +355,7 @@ data.
|
||||||
"version": <version>,
|
"version": <version>,
|
||||||
"name": <name>, // optional
|
"name": <name>, // optional
|
||||||
"color": <color>, // optional
|
"color": <color>, // optional
|
||||||
|
"order": <order> // optional
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2288,7 +2290,8 @@ experience.
|
||||||
"version": <version>,
|
"version": <version>,
|
||||||
"name": <name>,
|
"name": <name>,
|
||||||
"color": <color>,
|
"color": <color>,
|
||||||
"usages": <usages>
|
"usages": <usages>,
|
||||||
|
"order": <order>,
|
||||||
"default": <is-default>
|
"default": <is-default>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -2299,6 +2302,7 @@ experience.
|
||||||
- `<name>`: the category name.
|
- `<name>`: the category name.
|
||||||
- `<color>`: the category color.
|
- `<color>`: the category color.
|
||||||
- `<usages>`: how many tags is the given category used with.
|
- `<usages>`: how many tags is the given category used with.
|
||||||
|
- `<order>`: the order in which tags with this category are displayed, ascending.
|
||||||
- `<is-default>`: whether the tag category is the default one.
|
- `<is-default>`: whether the tag category is the default one.
|
||||||
|
|
||||||
## Tag
|
## Tag
|
||||||
|
@ -2498,7 +2502,7 @@ experience.
|
||||||
"version": <version>,
|
"version": <version>,
|
||||||
"name": <name>,
|
"name": <name>,
|
||||||
"color": <color>,
|
"color": <color>,
|
||||||
"usages": <usages>
|
"usages": <usages>,
|
||||||
"default": <is-default>
|
"default": <is-default>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -2712,7 +2716,7 @@ dictionaries as created by creation snapshots, which is described below.
|
||||||
},
|
},
|
||||||
"primitive-property":
|
"primitive-property":
|
||||||
{
|
{
|
||||||
"type": "primitive change":
|
"type": "primitive change",
|
||||||
"old-value": "<primitive>",
|
"old-value": "<primitive>",
|
||||||
"new-value": "<primitive>"
|
"new-value": "<primitive>"
|
||||||
},
|
},
|
||||||
|
|
|
@ -130,6 +130,7 @@ privileges:
|
||||||
'tag_categories:create': moderator
|
'tag_categories:create': moderator
|
||||||
'tag_categories:edit:name': moderator
|
'tag_categories:edit:name': moderator
|
||||||
'tag_categories:edit:color': moderator
|
'tag_categories:edit:color': moderator
|
||||||
|
'tag_categories:edit:order': moderator
|
||||||
'tag_categories:list': anonymous
|
'tag_categories:list': anonymous
|
||||||
'tag_categories:view': anonymous
|
'tag_categories:view': anonymous
|
||||||
'tag_categories:delete': moderator
|
'tag_categories:delete': moderator
|
||||||
|
|
|
@ -37,7 +37,8 @@ def create_tag_category(
|
||||||
auth.verify_privilege(ctx.user, "tag_categories:create")
|
auth.verify_privilege(ctx.user, "tag_categories:create")
|
||||||
name = ctx.get_param_as_string("name")
|
name = ctx.get_param_as_string("name")
|
||||||
color = ctx.get_param_as_string("color")
|
color = ctx.get_param_as_string("color")
|
||||||
category = tag_categories.create_category(name, color)
|
order = ctx.get_param_as_int("order")
|
||||||
|
category = tag_categories.create_category(name, color, order)
|
||||||
ctx.session.add(category)
|
ctx.session.add(category)
|
||||||
ctx.session.flush()
|
ctx.session.flush()
|
||||||
snapshots.create(category, ctx.user)
|
snapshots.create(category, ctx.user)
|
||||||
|
@ -73,6 +74,11 @@ def update_tag_category(
|
||||||
tag_categories.update_category_color(
|
tag_categories.update_category_color(
|
||||||
category, ctx.get_param_as_string("color")
|
category, ctx.get_param_as_string("color")
|
||||||
)
|
)
|
||||||
|
if ctx.has_param("order"):
|
||||||
|
auth.verify_privilege(ctx.user, "tag_categories:edit:order")
|
||||||
|
tag_categories.update_category_order(
|
||||||
|
category, ctx.get_param_as_int("order")
|
||||||
|
)
|
||||||
ctx.session.flush()
|
ctx.session.flush()
|
||||||
snapshots.modify(category, ctx.user)
|
snapshots.modify(category, ctx.user)
|
||||||
ctx.session.commit()
|
ctx.session.commit()
|
||||||
|
|
|
@ -48,6 +48,7 @@ class TagCategorySerializer(serialization.BaseSerializer):
|
||||||
"color": self.serialize_color,
|
"color": self.serialize_color,
|
||||||
"usages": self.serialize_usages,
|
"usages": self.serialize_usages,
|
||||||
"default": self.serialize_default,
|
"default": self.serialize_default,
|
||||||
|
"order": self.serialize_order,
|
||||||
}
|
}
|
||||||
|
|
||||||
def serialize_name(self) -> Any:
|
def serialize_name(self) -> Any:
|
||||||
|
@ -65,6 +66,9 @@ class TagCategorySerializer(serialization.BaseSerializer):
|
||||||
def serialize_default(self) -> Any:
|
def serialize_default(self) -> Any:
|
||||||
return self.category.default
|
return self.category.default
|
||||||
|
|
||||||
|
def serialize_order(self) -> Any:
|
||||||
|
return self.category.order
|
||||||
|
|
||||||
|
|
||||||
def serialize_category(
|
def serialize_category(
|
||||||
category: Optional[model.TagCategory], options: List[str] = []
|
category: Optional[model.TagCategory], options: List[str] = []
|
||||||
|
@ -74,10 +78,11 @@ def serialize_category(
|
||||||
return TagCategorySerializer(category).serialize(options)
|
return TagCategorySerializer(category).serialize(options)
|
||||||
|
|
||||||
|
|
||||||
def create_category(name: str, color: str) -> model.TagCategory:
|
def create_category(name: str, color: str, order: int) -> model.TagCategory:
|
||||||
category = model.TagCategory()
|
category = model.TagCategory()
|
||||||
update_category_name(category, name)
|
update_category_name(category, name)
|
||||||
update_category_color(category, color)
|
update_category_color(category, color)
|
||||||
|
update_category_order(category, order)
|
||||||
if not get_all_categories():
|
if not get_all_categories():
|
||||||
category.default = True
|
category.default = True
|
||||||
return category
|
return category
|
||||||
|
@ -117,6 +122,11 @@ def update_category_color(category: model.TagCategory, color: str) -> None:
|
||||||
category.color = color
|
category.color = color
|
||||||
|
|
||||||
|
|
||||||
|
def update_category_order(category: model.TagCategory, order: int) -> None:
|
||||||
|
assert category
|
||||||
|
category.order = order
|
||||||
|
|
||||||
|
|
||||||
def try_get_category_by_name(
|
def try_get_category_by_name(
|
||||||
name: str, lock: bool = False
|
name: str, lock: bool = False
|
||||||
) -> Optional[model.TagCategory]:
|
) -> Optional[model.TagCategory]:
|
||||||
|
|
|
@ -67,6 +67,7 @@ def sort_tags(tags: List[model.Tag]) -> List[model.Tag]:
|
||||||
return sorted(
|
return sorted(
|
||||||
tags,
|
tags,
|
||||||
key=lambda tag: (
|
key=lambda tag: (
|
||||||
|
tag.category.order,
|
||||||
default_category_name == tag.category.name,
|
default_category_name == tag.category.name,
|
||||||
tag.category.name,
|
tag.category.name,
|
||||||
tag.names[0].name,
|
tag.names[0].name,
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
"""
|
||||||
|
Add order column to tag categories.
|
||||||
|
|
||||||
|
Revision ID: c97dc1bf184a
|
||||||
|
Created at: 2020-09-19 17:08:03.225667
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "c97dc1bf184a"
|
||||||
|
down_revision = "54de8acc6cef"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column(
|
||||||
|
"tag_category", sa.Column("order", sa.Integer, nullable=True)
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
sa.table("tag_category", sa.column("order")).update().values(order=1)
|
||||||
|
)
|
||||||
|
op.alter_column("tag_category", "order", nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column("tag_category", "order")
|
|
@ -16,6 +16,7 @@ class TagCategory(Base):
|
||||||
"color", sa.Unicode(32), nullable=False, default="#000000"
|
"color", sa.Unicode(32), nullable=False, default="#000000"
|
||||||
)
|
)
|
||||||
default = sa.Column("default", sa.Boolean, nullable=False, default=False)
|
default = sa.Column("default", sa.Boolean, nullable=False, default=False)
|
||||||
|
order = sa.Column("order", sa.Integer, nullable=False, default=1)
|
||||||
|
|
||||||
def __init__(self, name: Optional[str] = None) -> None:
|
def __init__(self, name: Optional[str] = None) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
|
@ -36,11 +36,14 @@ def test_creating_category(
|
||||||
tag_categories.serialize_category.return_value = "serialized category"
|
tag_categories.serialize_category.return_value = "serialized category"
|
||||||
result = api.tag_category_api.create_tag_category(
|
result = api.tag_category_api.create_tag_category(
|
||||||
context_factory(
|
context_factory(
|
||||||
params={"name": "meta", "color": "black"}, user=auth_user
|
params={"name": "meta", "color": "black", "order": 0},
|
||||||
|
user=auth_user,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert result == "serialized category"
|
assert result == "serialized category"
|
||||||
tag_categories.create_category.assert_called_once_with("meta", "black")
|
tag_categories.create_category.assert_called_once_with(
|
||||||
|
"meta", "black", 0
|
||||||
|
)
|
||||||
snapshots.create.assert_called_once_with(category, auth_user)
|
snapshots.create.assert_called_once_with(category, auth_user)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@ def test_retrieving_single(
|
||||||
"color": "dummy",
|
"color": "dummy",
|
||||||
"usages": 0,
|
"usages": 0,
|
||||||
"default": False,
|
"default": False,
|
||||||
|
"order": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ def inject_config(config_injector):
|
||||||
"privileges": {
|
"privileges": {
|
||||||
"tag_categories:edit:name": model.User.RANK_REGULAR,
|
"tag_categories:edit:name": model.User.RANK_REGULAR,
|
||||||
"tag_categories:edit:color": model.User.RANK_REGULAR,
|
"tag_categories:edit:color": model.User.RANK_REGULAR,
|
||||||
|
"tag_categories:edit:order": model.User.RANK_REGULAR,
|
||||||
"tag_categories:set_default": model.User.RANK_REGULAR,
|
"tag_categories:set_default": model.User.RANK_REGULAR,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,10 +126,11 @@ def user_token_factory(user_factory):
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tag_category_factory():
|
def tag_category_factory():
|
||||||
def factory(name=None, color="dummy", default=False):
|
def factory(name=None, color="dummy", order=1, default=False):
|
||||||
category = model.TagCategory()
|
category = model.TagCategory()
|
||||||
category.name = name or get_unique_name()
|
category.name = name or get_unique_name()
|
||||||
category.color = color
|
category.color = color
|
||||||
|
category.order = order
|
||||||
category.default = default
|
category.default = default
|
||||||
return category
|
return category
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ def test_serialize_category(tag_category_factory, tag_factory):
|
||||||
"color": "color",
|
"color": "color",
|
||||||
"default": True,
|
"default": True,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
|
"order": 1,
|
||||||
"usages": 2,
|
"usages": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,8 +37,8 @@ def test_serialize_category(tag_category_factory, tag_factory):
|
||||||
def test_create_category_when_first():
|
def test_create_category_when_first():
|
||||||
with patch("szurubooru.func.tag_categories.update_category_name"), patch(
|
with patch("szurubooru.func.tag_categories.update_category_name"), patch(
|
||||||
"szurubooru.func.tag_categories.update_category_color"
|
"szurubooru.func.tag_categories.update_category_color"
|
||||||
):
|
), patch("szurubooru.func.tag_categories.update_category_order"):
|
||||||
category = tag_categories.create_category("name", "color")
|
category = tag_categories.create_category("name", "color", 7)
|
||||||
assert category.default
|
assert category.default
|
||||||
tag_categories.update_category_name.assert_called_once_with(
|
tag_categories.update_category_name.assert_called_once_with(
|
||||||
category, "name"
|
category, "name"
|
||||||
|
@ -45,6 +46,9 @@ def test_create_category_when_first():
|
||||||
tag_categories.update_category_color.assert_called_once_with(
|
tag_categories.update_category_color.assert_called_once_with(
|
||||||
category, "color"
|
category, "color"
|
||||||
)
|
)
|
||||||
|
tag_categories.update_category_order.assert_called_once_with(
|
||||||
|
category, 7
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_category_when_subsequent(tag_category_factory):
|
def test_create_category_when_subsequent(tag_category_factory):
|
||||||
|
@ -52,8 +56,8 @@ def test_create_category_when_subsequent(tag_category_factory):
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
with patch("szurubooru.func.tag_categories.update_category_name"), patch(
|
with patch("szurubooru.func.tag_categories.update_category_name"), patch(
|
||||||
"szurubooru.func.tag_categories.update_category_color"
|
"szurubooru.func.tag_categories.update_category_color"
|
||||||
):
|
), patch("szurubooru.func.tag_categories.update_category_order"):
|
||||||
category = tag_categories.create_category("name", "color")
|
category = tag_categories.create_category("name", "color", 7)
|
||||||
assert not category.default
|
assert not category.default
|
||||||
tag_categories.update_category_name.assert_called_once_with(
|
tag_categories.update_category_name.assert_called_once_with(
|
||||||
category, "name"
|
category, "name"
|
||||||
|
@ -61,6 +65,9 @@ def test_create_category_when_subsequent(tag_category_factory):
|
||||||
tag_categories.update_category_color.assert_called_once_with(
|
tag_categories.update_category_color.assert_called_once_with(
|
||||||
category, "color"
|
category, "color"
|
||||||
)
|
)
|
||||||
|
tag_categories.update_category_order.assert_called_once_with(
|
||||||
|
category, 7
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_update_category_name_with_empty_string(tag_category_factory):
|
def test_update_category_name_with_empty_string(tag_category_factory):
|
||||||
|
|
Loading…
Reference in a new issue