diff --git a/client/html/tag_categories.tpl b/client/html/tag_categories.tpl
index fe6b8987..f9d28e15 100644
--- a/client/html/tag_categories.tpl
+++ b/client/html/tag_categories.tpl
@@ -7,6 +7,7 @@
Category name |
CSS color |
+ Order |
Usages |
@@ -21,7 +22,7 @@
- <% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canDelete) { %>
+ <% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canEditOrder || ctx.canDelete) { %>
diff --git a/client/html/tag_category_row.tpl b/client/html/tag_category_row.tpl
index dcc8c16a..ce7aa59c 100644
--- a/client/html/tag_category_row.tpl
+++ b/client/html/tag_category_row.tpl
@@ -17,6 +17,13 @@
<%- ctx.tagCategory.color %>
<% } %>
+
+ <% if (ctx.canEditOrder) { %>
+ <%= ctx.makeNumericInput({value: ctx.tagCategory.order}) %>
+ <% } else { %>
+ <%- ctx.tagCategory.order %>
+ <% } %>
+ |
<% if (ctx.tagCategory.name) { %>
'>
diff --git a/client/js/controllers/tag_categories_controller.js b/client/js/controllers/tag_categories_controller.js
index 2470edbb..1ca743fd 100644
--- a/client/js/controllers/tag_categories_controller.js
+++ b/client/js/controllers/tag_categories_controller.js
@@ -26,6 +26,7 @@ class TagCategoriesController {
tagCategories: this._tagCategories,
canEditName: api.hasPrivilege("tagCategories:edit:name"),
canEditColor: api.hasPrivilege("tagCategories:edit:color"),
+ canEditOrder: api.hasPrivilege("tagCategories:edit:order"),
canDelete: api.hasPrivilege("tagCategories:delete"),
canCreate: api.hasPrivilege("tagCategories:create"),
canSetDefault: api.hasPrivilege(
diff --git a/client/js/models/tag_category.js b/client/js/models/tag_category.js
index a8d0e64c..1641862b 100644
--- a/client/js/models/tag_category.js
+++ b/client/js/models/tag_category.js
@@ -9,10 +9,12 @@ class TagCategory extends events.EventTarget {
super();
this._name = "";
this._color = "#000000";
+ this._order = 1;
this._tagCount = 0;
this._isDefault = false;
this._origName = null;
this._origColor = null;
+ this._origOrder = null;
}
get name() {
@@ -23,6 +25,10 @@ class TagCategory extends events.EventTarget {
return this._color;
}
+ get order() {
+ return this._order;
+ }
+
get tagCount() {
return this._tagCount;
}
@@ -43,6 +49,10 @@ class TagCategory extends events.EventTarget {
this._color = value;
}
+ set order(value) {
+ this._order = value;
+ }
+
static fromResponse(response) {
const ret = new TagCategory();
ret._updateFromResponse(response);
@@ -58,6 +68,9 @@ class TagCategory extends events.EventTarget {
if (this.color !== this._origColor) {
detail.color = this.color;
}
+ if (this.order !== this._origOrder) {
+ detail.order = this.order;
+ }
if (!Object.keys(detail).length) {
return Promise.resolve();
@@ -104,10 +117,12 @@ class TagCategory extends events.EventTarget {
this._version = response.version;
this._name = response.name;
this._color = response.color;
+ this._order = response.order;
this._isDefault = response.default;
this._tagCount = response.usages;
this._origName = this.name;
this._origColor = this.color;
+ this._origOrder = this.order;
}
}
diff --git a/client/js/views/tag_categories_view.js b/client/js/views/tag_categories_view.js
index 1f1a4dac..7cd5c19e 100644
--- a/client/js/views/tag_categories_view.js
+++ b/client/js/views/tag_categories_view.js
@@ -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");
if (removeLinkNode) {
removeLinkNode.addEventListener("click", (e) =>
@@ -147,6 +154,10 @@ class TagCategoriesView extends events.EventTarget {
rowNode._tagCategory.color = e.target.value;
}
+ _evtOrderChange(e, rowNode) {
+ rowNode._tagCategory.order = e.target.value;
+ }
+
_evtDeleteButtonClick(e, rowNode, link) {
e.preventDefault();
if (e.target.classList.contains("inactive")) {
diff --git a/doc/API.md b/doc/API.md
index 11f2d5bf..cccbc4e7 100644
--- a/doc/API.md
+++ b/doc/API.md
@@ -321,7 +321,8 @@ data.
```json5
{
"name": ,
- "color":
+ "color": ,
+ "order": // optional
}
```
@@ -354,6 +355,7 @@ data.
"version": ,
"name": , // optional
"color": , // optional
+ "order": // optional
}
```
@@ -2288,7 +2290,8 @@ experience.
"version": ,
"name": ,
"color": ,
- "usages":
+ "usages": ,
+ "order": ,
"default":
}
```
@@ -2299,6 +2302,7 @@ experience.
- ``: the category name.
- ``: the category color.
- ``: how many tags is the given category used with.
+- ``: the order in which tags with this category are displayed, ascending.
- ``: whether the tag category is the default one.
## Tag
@@ -2498,7 +2502,7 @@ experience.
"version": ,
"name": ,
"color": ,
- "usages":
+ "usages": ,
"default":
}
```
@@ -2712,7 +2716,7 @@ dictionaries as created by creation snapshots, which is described below.
},
"primitive-property":
{
- "type": "primitive change":
+ "type": "primitive change",
"old-value": "",
"new-value": ""
},
diff --git a/server/config.yaml.dist b/server/config.yaml.dist
index e3799a35..bc4e3630 100644
--- a/server/config.yaml.dist
+++ b/server/config.yaml.dist
@@ -130,6 +130,7 @@ privileges:
'tag_categories:create': moderator
'tag_categories:edit:name': moderator
'tag_categories:edit:color': moderator
+ 'tag_categories:edit:order': moderator
'tag_categories:list': anonymous
'tag_categories:view': anonymous
'tag_categories:delete': moderator
diff --git a/server/szurubooru/api/tag_category_api.py b/server/szurubooru/api/tag_category_api.py
index 498289a4..95d0ed8d 100644
--- a/server/szurubooru/api/tag_category_api.py
+++ b/server/szurubooru/api/tag_category_api.py
@@ -37,7 +37,8 @@ def create_tag_category(
auth.verify_privilege(ctx.user, "tag_categories:create")
name = ctx.get_param_as_string("name")
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.flush()
snapshots.create(category, ctx.user)
@@ -73,6 +74,11 @@ def update_tag_category(
tag_categories.update_category_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()
snapshots.modify(category, ctx.user)
ctx.session.commit()
diff --git a/server/szurubooru/func/tag_categories.py b/server/szurubooru/func/tag_categories.py
index bbf72978..16962d9a 100644
--- a/server/szurubooru/func/tag_categories.py
+++ b/server/szurubooru/func/tag_categories.py
@@ -48,6 +48,7 @@ class TagCategorySerializer(serialization.BaseSerializer):
"color": self.serialize_color,
"usages": self.serialize_usages,
"default": self.serialize_default,
+ "order": self.serialize_order,
}
def serialize_name(self) -> Any:
@@ -65,6 +66,9 @@ class TagCategorySerializer(serialization.BaseSerializer):
def serialize_default(self) -> Any:
return self.category.default
+ def serialize_order(self) -> Any:
+ return self.category.order
+
def serialize_category(
category: Optional[model.TagCategory], options: List[str] = []
@@ -74,10 +78,11 @@ def serialize_category(
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()
update_category_name(category, name)
update_category_color(category, color)
+ update_category_order(category, order)
if not get_all_categories():
category.default = True
return category
@@ -117,6 +122,11 @@ def update_category_color(category: model.TagCategory, color: str) -> None:
category.color = color
+def update_category_order(category: model.TagCategory, order: int) -> None:
+ assert category
+ category.order = order
+
+
def try_get_category_by_name(
name: str, lock: bool = False
) -> Optional[model.TagCategory]:
diff --git a/server/szurubooru/func/tags.py b/server/szurubooru/func/tags.py
index e15a78cc..28a2a76b 100644
--- a/server/szurubooru/func/tags.py
+++ b/server/szurubooru/func/tags.py
@@ -67,6 +67,7 @@ def sort_tags(tags: List[model.Tag]) -> List[model.Tag]:
return sorted(
tags,
key=lambda tag: (
+ tag.category.order,
default_category_name == tag.category.name,
tag.category.name,
tag.names[0].name,
diff --git a/server/szurubooru/migrations/versions/c97dc1bf184a_add_order_column_to_tag_categories.py b/server/szurubooru/migrations/versions/c97dc1bf184a_add_order_column_to_tag_categories.py
new file mode 100644
index 00000000..c5a31249
--- /dev/null
+++ b/server/szurubooru/migrations/versions/c97dc1bf184a_add_order_column_to_tag_categories.py
@@ -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")
diff --git a/server/szurubooru/model/tag_category.py b/server/szurubooru/model/tag_category.py
index 1faf74ca..a336a214 100644
--- a/server/szurubooru/model/tag_category.py
+++ b/server/szurubooru/model/tag_category.py
@@ -16,6 +16,7 @@ class TagCategory(Base):
"color", sa.Unicode(32), nullable=False, default="#000000"
)
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:
self.name = name
diff --git a/server/szurubooru/tests/api/test_tag_category_creating.py b/server/szurubooru/tests/api/test_tag_category_creating.py
index 2370cb83..6798cbe2 100644
--- a/server/szurubooru/tests/api/test_tag_category_creating.py
+++ b/server/szurubooru/tests/api/test_tag_category_creating.py
@@ -36,11 +36,14 @@ def test_creating_category(
tag_categories.serialize_category.return_value = "serialized category"
result = api.tag_category_api.create_tag_category(
context_factory(
- params={"name": "meta", "color": "black"}, user=auth_user
+ params={"name": "meta", "color": "black", "order": 0},
+ user=auth_user,
)
)
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)
diff --git a/server/szurubooru/tests/api/test_tag_category_retrieving.py b/server/szurubooru/tests/api/test_tag_category_retrieving.py
index 40d9059b..cec3657f 100644
--- a/server/szurubooru/tests/api/test_tag_category_retrieving.py
+++ b/server/szurubooru/tests/api/test_tag_category_retrieving.py
@@ -46,6 +46,7 @@ def test_retrieving_single(
"color": "dummy",
"usages": 0,
"default": False,
+ "order": 1,
"version": 1,
}
diff --git a/server/szurubooru/tests/api/test_tag_category_updating.py b/server/szurubooru/tests/api/test_tag_category_updating.py
index 41346131..f12ce367 100644
--- a/server/szurubooru/tests/api/test_tag_category_updating.py
+++ b/server/szurubooru/tests/api/test_tag_category_updating.py
@@ -17,6 +17,7 @@ def inject_config(config_injector):
"privileges": {
"tag_categories:edit:name": 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,
},
}
diff --git a/server/szurubooru/tests/conftest.py b/server/szurubooru/tests/conftest.py
index 481d884a..e7811fe1 100644
--- a/server/szurubooru/tests/conftest.py
+++ b/server/szurubooru/tests/conftest.py
@@ -126,10 +126,11 @@ def user_token_factory(user_factory):
@pytest.fixture
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.name = name or get_unique_name()
category.color = color
+ category.order = order
category.default = default
return category
diff --git a/server/szurubooru/tests/func/test_tag_categories.py b/server/szurubooru/tests/func/test_tag_categories.py
index 143cc49f..11300cf4 100644
--- a/server/szurubooru/tests/func/test_tag_categories.py
+++ b/server/szurubooru/tests/func/test_tag_categories.py
@@ -29,6 +29,7 @@ def test_serialize_category(tag_category_factory, tag_factory):
"color": "color",
"default": True,
"version": 1,
+ "order": 1,
"usages": 2,
}
@@ -36,8 +37,8 @@ def test_serialize_category(tag_category_factory, tag_factory):
def test_create_category_when_first():
with patch("szurubooru.func.tag_categories.update_category_name"), patch(
"szurubooru.func.tag_categories.update_category_color"
- ):
- category = tag_categories.create_category("name", "color")
+ ), patch("szurubooru.func.tag_categories.update_category_order"):
+ category = tag_categories.create_category("name", "color", 7)
assert category.default
tag_categories.update_category_name.assert_called_once_with(
category, "name"
@@ -45,6 +46,9 @@ def test_create_category_when_first():
tag_categories.update_category_color.assert_called_once_with(
category, "color"
)
+ tag_categories.update_category_order.assert_called_once_with(
+ category, 7
+ )
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()
with patch("szurubooru.func.tag_categories.update_category_name"), patch(
"szurubooru.func.tag_categories.update_category_color"
- ):
- category = tag_categories.create_category("name", "color")
+ ), patch("szurubooru.func.tag_categories.update_category_order"):
+ category = tag_categories.create_category("name", "color", 7)
assert not category.default
tag_categories.update_category_name.assert_called_once_with(
category, "name"
@@ -61,6 +65,9 @@ def test_create_category_when_subsequent(tag_category_factory):
tag_categories.update_category_color.assert_called_once_with(
category, "color"
)
+ tag_categories.update_category_order.assert_called_once_with(
+ category, 7
+ )
def test_update_category_name_with_empty_string(tag_category_factory):
|