server/posts: support aspect-ratio search query

This commit is contained in:
rr- 2017-02-05 22:09:33 +01:00
parent 0b21d98c9b
commit 00c3a4320b
6 changed files with 120 additions and 62 deletions

6
API.md
View file

@ -697,7 +697,7 @@ data.
**Named tokens** **Named tokens**
| `<key>` | Description | | `<key>` | Description |
| ------------------ | ---------------------------------------------------------- | | -------------------- | ---------------------------------------------------------- |
| `id` | having given post number | | `id` | having given post number |
| `tag` | having given tag (accepts wildcards) | | `tag` | having given tag (accepts wildcards) |
| `score` | having given score | | `score` | having given score |
@ -719,9 +719,13 @@ data.
| `image-width` | having given image width (where applicable) | | `image-width` | having given image width (where applicable) |
| `image-height` | having given image height (where applicable) | | `image-height` | having given image height (where applicable) |
| `image-area` | having given number of pixels (image width * image height) | | `image-area` | having given number of pixels (image width * image height) |
| `image-aspect-ratio` | having given aspect ratio (image width / image height) |
| `image-ar` | alias of `image-aspect-ratio` |
| `width` | alias of `image-width` | | `width` | alias of `image-width` |
| `height` | alias of `image-height` | | `height` | alias of `image-height` |
| `area` | alias of `image-area` | | `area` | alias of `image-area` |
| `ar` | alias of `image-aspect-ratio` |
| `aspect-ratio` | alias of `image-aspect-ratio` |
| `creation-date` | posted at given date | | `creation-date` | posted at given date |
| `creation-time` | alias of `creation-date` | | `creation-time` | alias of `creation-date` |
| `date` | alias of `creation-date` | | `date` | alias of `creation-date` |

View file

@ -90,6 +90,14 @@
<td><code>image-area</code></td> <td><code>image-area</code></td>
<td>having given number of pixels (image width * image height)</td> <td>having given number of pixels (image width * image height)</td>
</tr> </tr>
<tr>
<td><code>image-aspect-ratio</code></td>
<td>having given aspect ratio (image width / image height)</td>
</tr>
<tr>
<td><code>image-ar</code></td>
<td>alias of <code>image-aspect-ratio</code></td>
</tr>
<tr> <tr>
<td><code>width</code></td> <td><code>width</code></td>
<td>alias of <code>image-width</code></td> <td>alias of <code>image-width</code></td>
@ -102,6 +110,14 @@
<td><code>area</code></td> <td><code>area</code></td>
<td>alias of <code>image-area</code></td> <td>alias of <code>image-area</code></td>
</tr> </tr>
<tr>
<td><code>aspect-ratio</code></td>
<td>alias of <code>image-aspect-ratio</code></td>
</tr>
<tr>
<td><code>ar</code></td>
<td>alias of <code>image-aspect-ratio</code></td>
</tr>
<tr> <tr>
<td><code>creation-date</code></td> <td><code>creation-date</code></td>
<td>posted at given date</td> <td>posted at given date</td>

View file

@ -194,6 +194,7 @@ class Post(Base):
.correlate_except(PostTag)) .correlate_except(PostTag))
canvas_area = column_property(canvas_width * canvas_height) canvas_area = column_property(canvas_width * canvas_height)
canvas_aspect_ratio = column_property(canvas_width / canvas_height)
@property @property
def is_featured(self) -> bool: def is_featured(self) -> bool:

View file

@ -290,6 +290,13 @@ class PostSearchConfig(BaseSearchConfig):
search_util.create_num_filter(model.Post.canvas_area) search_util.create_num_filter(model.Post.canvas_area)
), ),
(
['image-aspect-ratio', 'image-ar', 'aspect-ratio', 'ar'],
search_util.create_num_filter(
model.Post.canvas_aspect_ratio,
transformer=search_util.float_transformer)
),
( (
['creation-date', 'creation-time', 'date', 'time'], ['creation-date', 'creation-time', 'date', 'time'],
search_util.create_date_filter(model.Post.creation_time) search_util.create_date_filter(model.Post.creation_time)

View file

@ -1,4 +1,4 @@
from typing import Any, Optional, Callable from typing import Any, Optional, Union, Callable
import sqlalchemy as sa import sqlalchemy as sa
from szurubooru import db, errors from szurubooru import db, errors
from szurubooru.func import util from szurubooru.func import util
@ -7,6 +7,9 @@ from szurubooru.search.typing import SaColumn, SaQuery
from szurubooru.search.configs.base_search_config import Filter from szurubooru.search.configs.base_search_config import Filter
Number = Union[int, float]
def wildcard_transformer(value: str) -> str: def wildcard_transformer(value: str) -> str:
return ( return (
value value
@ -16,22 +19,37 @@ def wildcard_transformer(value: str) -> str:
.replace('*', '%')) .replace('*', '%'))
def integer_transformer(value: str) -> int:
return int(value)
def float_transformer(value: str) -> float:
for sep in list('/:'):
if sep in value:
a, b = value.split(sep, 1)
return float(a) / float(b)
return float(value)
def apply_num_criterion_to_column( def apply_num_criterion_to_column(
column: Any, criterion: criteria.BaseCriterion) -> Any: column: Any,
criterion: criteria.BaseCriterion,
transformer: Callable[[str], Number]=integer_transformer) -> SaQuery:
try: try:
if isinstance(criterion, criteria.PlainCriterion): if isinstance(criterion, criteria.PlainCriterion):
expr = column == int(criterion.value) expr = column == transformer(criterion.value)
elif isinstance(criterion, criteria.ArrayCriterion): elif isinstance(criterion, criteria.ArrayCriterion):
expr = column.in_(int(value) for value in criterion.values) expr = column.in_(transformer(value) for value in criterion.values)
elif isinstance(criterion, criteria.RangedCriterion): elif isinstance(criterion, criteria.RangedCriterion):
assert criterion.min_value or criterion.max_value assert criterion.min_value or criterion.max_value
if criterion.min_value and criterion.max_value: if criterion.min_value and criterion.max_value:
expr = column.between( expr = column.between(
int(criterion.min_value), int(criterion.max_value)) transformer(criterion.min_value),
transformer(criterion.max_value))
elif criterion.min_value: elif criterion.min_value:
expr = column >= int(criterion.min_value) expr = column >= transformer(criterion.min_value)
elif criterion.max_value: elif criterion.max_value:
expr = column <= int(criterion.max_value) expr = column <= transformer(criterion.max_value)
else: else:
assert False assert False
except ValueError: except ValueError:
@ -40,13 +58,15 @@ def apply_num_criterion_to_column(
return expr return expr
def create_num_filter(column: Any) -> Filter: def create_num_filter(
column: Any,
transformer: Callable[[str], Number]=integer_transformer) -> SaQuery:
def wrapper( def wrapper(
query: SaQuery, query: SaQuery,
criterion: Optional[criteria.BaseCriterion], criterion: Optional[criteria.BaseCriterion],
negated: bool) -> SaQuery: negated: bool) -> SaQuery:
assert criterion assert criterion
expr = apply_num_criterion_to_column(column, criterion) expr = apply_num_criterion_to_column(column, criterion, transformer)
if negated: if negated:
expr = ~expr expr = ~expr
return query.filter(expr) return query.filter(expr)

View file

@ -422,14 +422,19 @@ def test_filter_by_file_size(
@pytest.mark.parametrize('input,expected_post_ids', [ @pytest.mark.parametrize('input,expected_post_ids', [
('image-width:100', [1]), ('image-width:100', [1]),
('image-width:102', [3]), ('image-width:200', [2]),
('image-width:100,102', [1, 3]), ('image-width:100,300', [1, 3]),
('image-height:200', [1]), ('image-height:200', [1]),
('image-height:202', [3]), ('image-height:100', [2]),
('image-height:200,202', [1, 3]), ('image-height:200,300', [1, 3]),
('image-area:20000', [1]), ('image-area:20000', [1, 2]),
('image-area:20604', [3]), ('image-area:90000', [3]),
('image-area:20000,20604', [1, 3]), ('image-area:20000,90000', [1, 2, 3]),
('image-ar:1', [3]),
('image-ar:..0.9', [1]),
('image-ar:1.1..', [2]),
('image-ar:1/1..1/1', [3]),
('image-ar:1:1..1:1', [3]),
]) ])
def test_filter_by_image_size( def test_filter_by_image_size(
verify_unpaged, post_factory, input, expected_post_ids): verify_unpaged, post_factory, input, expected_post_ids):
@ -437,16 +442,21 @@ def test_filter_by_image_size(
post2 = post_factory(id=2) post2 = post_factory(id=2)
post3 = post_factory(id=3) post3 = post_factory(id=3)
post1.canvas_width = 100 post1.canvas_width = 100
post2.canvas_width = 101
post3.canvas_width = 102
post1.canvas_height = 200 post1.canvas_height = 200
post2.canvas_height = 201 post2.canvas_width = 200
post3.canvas_height = 202 post2.canvas_height = 100
post3.canvas_width = 300
post3.canvas_height = 300
db.session.add_all([post1, post2, post3]) db.session.add_all([post1, post2, post3])
db.session.flush() db.session.flush()
verify_unpaged(input, expected_post_ids) verify_unpaged(input, expected_post_ids)
def test_filter_by_invalid_aspect_ratio(executor):
with pytest.raises(errors.SearchError):
executor.execute('image-ar:1:1:1', page=1, page_size=100)
@pytest.mark.parametrize('input,expected_post_ids', [ @pytest.mark.parametrize('input,expected_post_ids', [
('creation-date:2014', [1]), ('creation-date:2014', [1]),
('creation-date:2016', [3]), ('creation-date:2016', [3]),