summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorHunternif <hunternif@gmail.com>2019-04-18 04:21:02 +0700
committerHunternif <hunternif@gmail.com>2019-04-18 04:21:02 +0700
commit5d115ad95fa7f094871c7b39c4c2bce86e659d99 (patch)
tree60a0795278f49339385e8da6944c0fe3412ae910 /server
parenta5d7897cec2a57333ab479d58ececd5e9bdaa9bb (diff)
server/test: create or update post metrics via updating post
Diffstat (limited to 'server')
-rw-r--r--server/config.yaml.dist5
-rw-r--r--server/szurubooru/api/post_api.py6
-rw-r--r--server/szurubooru/func/metrics.py84
-rw-r--r--server/szurubooru/model/metric.py6
-rw-r--r--server/szurubooru/tests/conftest.py22
-rw-r--r--server/szurubooru/tests/func/test_metrics.py107
-rw-r--r--server/szurubooru/tests/model/test_metric.py50
7 files changed, 259 insertions, 21 deletions
diff --git a/server/config.yaml.dist b/server/config.yaml.dist
index a68aea3..f180f34 100644
--- a/server/config.yaml.dist
+++ b/server/config.yaml.dist
@@ -117,8 +117,9 @@ privileges:
'tag_categories:delete': moderator
'tag_categories:set_default': moderator
- 'metrics:create': moderator
- 'metrics:edit:bounds': moderator
+ 'metrics:create': power
+ 'metrics:edit:bounds': power
+ 'metrics:edit:posts': regular
'comments:create': regular
'comments:delete:any': moderator
diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py
index 58d0708..914b70b 100644
--- a/server/szurubooru/api/post_api.py
+++ b/server/szurubooru/api/post_api.py
@@ -2,7 +2,7 @@ from typing import Optional, Dict, List
from datetime import datetime
from szurubooru import db, model, errors, rest, search
from szurubooru.func import (
- auth, tags, posts, snapshots, favorites, scores, serialization, versions)
+ auth, tags, posts, snapshots, favorites, scores, serialization, versions, metrics)
_search_executor_config = search.configs.PostSearchConfig()
@@ -135,6 +135,10 @@ def update_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
if ctx.has_file('thumbnail'):
auth.verify_privilege(ctx.user, 'posts:edit:thumbnail')
posts.update_post_thumbnail(post, ctx.get_file('thumbnail'))
+ if ctx.has_file('metrics'):
+ auth.verify_privilege(ctx.user, 'metrics:edit:posts')
+ metrics.update_or_create_post_metrics(post, ctx.get_param_as_list('metrics'))
+
post.last_edit_time = datetime.utcnow()
ctx.session.flush()
snapshots.modify(post, ctx.user)
diff --git a/server/szurubooru/func/metrics.py b/server/szurubooru/func/metrics.py
index ffb8906..e1c5e67 100644
--- a/server/szurubooru/func/metrics.py
+++ b/server/szurubooru/func/metrics.py
@@ -1,7 +1,10 @@
-from typing import Any, Optional, Tuple, List, Dict, Callable
-import sqlalchemy as sa
-from szurubooru import config, db, model, errors, rest
-from szurubooru.func import serialization
+from typing import Any, Optional, List, Dict, Callable
+from szurubooru import db, model, errors, rest
+from szurubooru.func import serialization, tags
+
+
+class MetricDoesNotExistsError(errors.ValidationError):
+ pass
class MetricAlreadyExistsError(errors.ValidationError):
@@ -12,7 +15,15 @@ class InvalidMetricError(errors.ValidationError):
pass
-class MetricSeralizer(serialization.BaseSerializer):
+class PostMissingTagError(errors.ValidationError):
+ pass
+
+
+class MetricValueOutOfRangeError(errors.ValidationError):
+ pass
+
+
+class MetricSerializer(serialization.BaseSerializer):
def __init__(self, metric: model.Metric):
self.metric = metric
@@ -29,16 +40,30 @@ class MetricSeralizer(serialization.BaseSerializer):
return self.metric.max
-def serialize_metric(metric: model.Metric, options: List[str] = []) -> Optional[rest.Response]:
+def serialize_metric(
+ metric: model.Metric,
+ options: List[str] = []) -> Optional[rest.Response]:
if not metric:
return None
- return MetricSeralizer(metric).serialize(options)
+ return MetricSerializer(metric).serialize(options)
+
+
+def try_get_post_metric(
+ post: model.Post,
+ metric: model.Metric) -> Optional[model.PostMetric]:
+ return (
+ db.session
+ .query(model.PostMetric)
+ .filter(model.PostMetric.metric == metric and
+ model.PostMetric.post == post)
+ .one_or_none())
def create_metric(
tag: model.Tag,
min: float,
max: float) -> model.Metric:
+ assert tag
if tag.metric is not None:
raise MetricAlreadyExistsError('Tag already has a metric.')
if min >= max:
@@ -48,7 +73,8 @@ def create_metric(
return metric
-def update_or_create_metric(tag: model.Tag, metric_data) -> Optional[model.Metric]:
+def update_or_create_metric(tag: model.Tag, metric_data: Any) -> Optional[model.Metric]:
+ assert tag
for field in ('min', 'max'):
if field not in metric_data:
raise InvalidMetricError('Metric is missing %r field.' % field)
@@ -61,6 +87,42 @@ def update_or_create_metric(tag: model.Tag, metric_data) -> Optional[model.Metri
tag.metric.max = max
return None
else:
- metric = model.Metric(tag=tag, min=min, max=max)
- db.session.add(metric)
- return metric
+ return create_metric(tag=tag, min=min, max=max)
+
+
+def update_or_create_post_metric(
+ post: model.Post,
+ metric: model.Metric,
+ value: float) -> model.PostMetric:
+ assert post
+ assert metric
+ if metric.tag not in post.tags:
+ raise PostMissingTagError('Post doesn\'t have tag %r' % metric.tag.names[0])
+ if value < metric.min or value > metric.max:
+ raise MetricValueOutOfRangeError('Metric value %r out of range.' % value)
+ db.session.query(model.PostMetric)
+ post_metric = try_get_post_metric(post, metric)
+ if post_metric is None:
+ post_metric = model.PostMetric(post=post, metric=metric, value=value)
+ db.session.add(post_metric)
+ else:
+ post_metric.value = value
+ return post_metric
+
+
+def update_or_create_post_metrics(post: model.Post, metrics_data: Any) -> None:
+ """
+ Overwrites any existing metric values, deletes other existing post metrics.
+ """
+ assert post
+ post.metrics = []
+ for metric_data in metrics_data:
+ for field in ('tag_name', 'value'):
+ if field not in metric_data:
+ raise InvalidMetricError('Metric is missing %r field.' % field)
+ value = float(metric_data['value'])
+ tag = tags.get_tag_by_name(metric_data['tag_name'])
+ if tag.metric is None:
+ raise MetricDoesNotExistsError('Tag %r has no metric.' % tag.names[0])
+ post_metric = update_or_create_post_metric(post, tag.metric, value)
+ post.metrics.append(post_metric)
diff --git a/server/szurubooru/model/metric.py b/server/szurubooru/model/metric.py
index 2ec5b2b..14e8949 100644
--- a/server/szurubooru/model/metric.py
+++ b/server/szurubooru/model/metric.py
@@ -23,6 +23,7 @@ class PostMetric(Base):
value = sa.Column('value', sa.Float, nullable=False, index=True)
post = sa.orm.relationship('Post')
+ metric = sa.orm.relationship('Metric', back_populates='post_metrics')
__mapper_args__ = {
'version_id_col': version,
@@ -56,6 +57,7 @@ class PostMetricRange(Base):
high = sa.Column('low', sa.Float, nullable=False)
post = sa.orm.relationship('Post')
+ metric = sa.orm.relationship('Metric', back_populates='post_metric_ranges')
__mapper_args__ = {
'version_id_col': version,
@@ -82,9 +84,9 @@ class Metric(Base):
tag = sa.orm.relationship('Tag')
post_metrics = sa.orm.relationship(
- 'PostMetric', backref='metric', cascade='all, delete-orphan')
+ 'PostMetric', back_populates='metric', cascade='all, delete-orphan')
post_metric_ranges = sa.orm.relationship(
- 'PostMetricRange', backref='metric', cascade='all, delete-orphan')
+ 'PostMetricRange', back_populates='metric', cascade='all, delete-orphan')
post_metric_count = sa.orm.column_property(
sa.sql.expression.select(
diff --git a/server/szurubooru/tests/conftest.py b/server/szurubooru/tests/conftest.py
index 27a107a..21e0f0f 100644
--- a/server/szurubooru/tests/conftest.py
+++ b/server/szurubooru/tests/conftest.py
@@ -252,6 +252,28 @@ def post_favorite_factory(user_factory, post_factory):
@pytest.fixture
+def metric_factory(tag_factory):
+ def factory(tag=None):
+ if tag is None:
+ tag = tag_factory()
+ return model.Metric(tag=tag, min=0, max=10)
+ return factory
+
+
+@pytest.fixture
+def post_metric_factory(post_factory, metric_factory):
+ def factory(post=None, metric=None, value=None):
+ if post is None:
+ post = post_factory()
+ if metric is None:
+ metric = metric_factory()
+ if value is None:
+ value = (metric.min + metric.max)/2
+ return model.PostMetric(post=post, metric=metric, value=value)
+ return factory
+
+
+@pytest.fixture
def read_asset():
def get(path):
path = os.path.join(os.path.dirname(__file__), 'assets', path)
diff --git a/server/szurubooru/tests/func/test_metrics.py b/server/szurubooru/tests/func/test_metrics.py
index 9b0bf02..f522136 100644
--- a/server/szurubooru/tests/func/test_metrics.py
+++ b/server/szurubooru/tests/func/test_metrics.py
@@ -13,13 +13,24 @@ def test_serialize_metric(tag_factory):
}
+def test_try_get_post_metric(
+ post_factory, metric_factory, post_metric_factory):
+ post = post_factory()
+ metric = metric_factory()
+ metric2 = metric_factory()
+ post_metric = post_metric_factory(post=post, metric=metric)
+ db.session.add_all([post, metric, metric2, post_metric])
+ db.session.flush()
+ assert metrics.try_get_post_metric(post, metric2) is None
+ assert metrics.try_get_post_metric(post, metric) is post_metric
+
+
def test_create_metric(tag_factory):
tag = tag_factory()
db.session.add(tag)
new_metric = metrics.create_metric(tag, 1, 2)
assert new_metric is not None
db.session.flush()
- db.session.refresh(tag)
assert tag.metric is not None
assert tag.metric.min == 1
assert tag.metric.max == 2
@@ -44,7 +55,6 @@ def test_update_or_create_metric(tag_factory):
new_metric = metrics.update_or_create_metric(tag, {'min': 1, 'max': 2})
assert new_metric is not None
db.session.flush()
- db.session.refresh(tag)
assert tag.metric is not None
assert tag.metric.min == 1
assert tag.metric.max == 2
@@ -52,7 +62,6 @@ def test_update_or_create_metric(tag_factory):
new_metric = metrics.update_or_create_metric(tag, {'min': 3, 'max': 4})
assert new_metric is None
db.session.flush()
- db.session.refresh(tag)
assert tag.metric.min == 3
assert tag.metric.max == 4
@@ -64,3 +73,95 @@ def test_update_or_create_metric_with_invalid_params(tag_factory, params):
tag = tag_factory()
with pytest.raises(metrics.InvalidMetricError):
metrics.update_or_create_metric(tag, params)
+
+
+def test_update_or_create_post_metric_without_tag(post_factory, metric_factory):
+ post = post_factory()
+ metric = metric_factory()
+ with pytest.raises(metrics.PostMissingTagError):
+ metrics.update_or_create_post_metric(post, metric, 1.5)
+
+
+def test_update_or_create_post_metric_with_invalid_value(
+ post_factory, tag_factory, metric_factory):
+ post = post_factory()
+ tag = tag_factory()
+ post.tags = [tag]
+ metric = metric_factory(tag)
+ with pytest.raises(metrics.MetricValueOutOfRangeError):
+ metrics.update_or_create_post_metric(post, metric, -99)
+
+
+def test_update_or_create_post_metric_create(
+ post_factory, tag_factory, metric_factory):
+ post = post_factory()
+ tag = tag_factory()
+ post.tags = [tag]
+ metric = metric_factory(tag)
+ db.session.add(metric)
+ db.session.flush()
+ post_metric = metrics.update_or_create_post_metric(post, metric, 1.5)
+ assert post_metric.value == 1.5
+
+
+def test_update_or_create_post_metric_update(
+ post_factory, tag_factory, metric_factory):
+ post = post_factory()
+ tag = tag_factory()
+ post.tags = [tag]
+ metric = metric_factory(tag)
+ post_metric = model.PostMetric(post=post, metric=metric, value=1.2)
+ db.session.add(post_metric)
+ db.session.flush()
+
+ metrics.update_or_create_post_metric(post, metric, 3.4)
+ db.session.flush()
+
+ assert post_metric.value == 3.4
+
+
+def test_update_or_create_post_metrics_missing_tag(
+ post_factory, tag_factory, metric_factory):
+ post = post_factory()
+ tag = tag_factory(names=['tag1'])
+ metric = metric_factory(tag)
+ db.session.add(metric)
+ db.session.flush()
+ data = [{'tag_name': 'tag1', 'value': 1.5}]
+ with pytest.raises(metrics.PostMissingTagError):
+ metrics.update_or_create_post_metrics(post, data)
+
+
+@pytest.mark.parametrize('params', [
+ [{}],
+ [{'tag_name': 'tag'}],
+ [{'value': 1.5}]
+])
+def test_update_or_create_post_metrics_with_invalid_params(
+ params, post_factory):
+ post = post_factory()
+ with pytest.raises(metrics.InvalidMetricError):
+ metrics.update_or_create_post_metrics(post, params)
+
+
+def test_update_or_create_post_metrics(
+ post_factory, tag_factory, metric_factory):
+ post = post_factory()
+ tag1 = tag_factory(names=['tag1'])
+ tag2 = tag_factory(names=['tag2'])
+ post.tags = [tag1, tag2]
+ metric1 = metric_factory(tag1)
+ metric2 = metric_factory(tag2)
+ db.session.add_all([metric1, metric2])
+ db.session.flush()
+
+ data = [
+ {'tag_name': 'tag1', 'value': 1.2},
+ {'tag_name': 'tag2', 'value': 3.4},
+ ]
+ metrics.update_or_create_post_metrics(post, data)
+ db.session.flush()
+
+ assert len(post.metrics) == 2
+ assert post.metrics[0].value == 1.2
+ assert post.metrics[1].value == 3.4
diff --git a/server/szurubooru/tests/model/test_metric.py b/server/szurubooru/tests/model/test_metric.py
index d388031..7205f03 100644
--- a/server/szurubooru/tests/model/test_metric.py
+++ b/server/szurubooru/tests/model/test_metric.py
@@ -131,6 +131,43 @@ def test_cascade_delete_post(post_factory, tag_factory):
assert db.session.query(model.PostMetric).count() == 1
+def test_delete_post_metric_no_cascade(metric_factory, post_metric_factory):
+ metric = metric_factory()
+ post_metric = post_metric_factory(metric=metric)
+ db.session.add_all([metric, post_metric])
+ db.session.commit()
+ db.session.refresh(metric)
+ assert len(metric.post_metrics) == 1
+
+ db.session.delete(post_metric)
+ db.session.commit()
+ db.session.refresh(metric)
+ assert len(metric.post_metrics) == 0
+
+
+def test_cascade_delete_on_remove_metric_from_post(
+ post_factory, post_metric_factory):
+ post = post_factory()
+ post_metric = post_metric_factory(post=post)
+ db.session.add_all([post, post_metric])
+ db.session.commit()
+
+ assert not db.session.dirty
+ assert db.session.query(model.Post).count() == 1
+ assert db.session.query(model.Tag).count() == 1
+ assert db.session.query(model.Metric).count() == 1
+ assert db.session.query(model.PostMetric).count() == 1
+
+ post.metrics.clear()
+ db.session.commit()
+
+ assert not db.session.dirty
+ assert db.session.query(model.Post).count() == 1
+ assert db.session.query(model.Tag).count() == 1
+ assert db.session.query(model.Metric).count() == 1
+ assert db.session.query(model.PostMetric).count() == 0
+
+
def test_tag_without_metric(tag_factory):
tag = tag_factory(names=['mytag'])
assert tag.metric is None
@@ -145,5 +182,14 @@ def test_tag_without_metric(tag_factory):
assert tag.metric is None
-def test_metric_counts():
- pass
+def test_metric_counts(post_factory, metric_factory):
+ post1 = post_factory()
+ post2 = post_factory()
+ metric = metric_factory()
+ post_metric1 = model.PostMetric(post=post1, metric=metric, value=1.2)
+ post_metric2 = model.PostMetric(post=post2, metric=metric, value=3.4)
+ post_metric_range = model.PostMetricRange(post=post1, metric=metric, low=5.6, high=7.8)
+ db.session.add_all([metric, post_metric1, post_metric2, post_metric_range])
+ db.session.flush()
+ assert metric.post_metric_count == 2
+ assert metric.post_metric_range_count == 1