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

88
API.md
View file

@ -696,48 +696,52 @@ 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 |
| `uploader` | uploaded by given user (accepts wildcards) | | `uploader` | uploaded by given user (accepts wildcards) |
| `upload` | alias of upload | | `upload` | alias of upload |
| `submit` | alias of upload | | `submit` | alias of upload |
| `comment` | commented by given user (accepts wildcards) | | `comment` | commented by given user (accepts wildcards) |
| `fav` | favorited by given user (accepts wildcards) | | `fav` | favorited by given user (accepts wildcards) |
| `tag-count` | having given number of tags | | `tag-count` | having given number of tags |
| `comment-count` | having given number of comments | | `comment-count` | having given number of comments |
| `fav-count` | favorited by given number of users | | `fav-count` | favorited by given number of users |
| `note-count` | having given number of annotations | | `note-count` | having given number of annotations |
| `note-text` | having given note text (accepts wildcards) | | `note-text` | having given note text (accepts wildcards) |
| `relation-count` | having given number of relations | | `relation-count` | having given number of relations |
| `feature-count` | having been featured given number of times | | `feature-count` | having been featured given number of times |
| `type` | given type of posts. `<value>` can be either `image`, `animation` (or `animated` or `anim`), `flash` (or `swf`) or `video` (or `webm`). | | `type` | given type of posts. `<value>` can be either `image`, `animation` (or `animated` or `anim`), `flash` (or `swf`) or `video` (or `webm`). |
| `content-checksum` | having given SHA1 checksum | | `content-checksum` | having given SHA1 checksum |
| `file-size` | having given file size (in bytes) | | `file-size` | having given file size (in bytes) |
| `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) |
| `width` | alias of `image-width` | | `image-aspect-ratio` | having given aspect ratio (image width / image height) |
| `height` | alias of `image-height` | | `image-ar` | alias of `image-aspect-ratio` |
| `area` | alias of `image-area` | | `width` | alias of `image-width` |
| `creation-date` | posted at given date | | `height` | alias of `image-height` |
| `creation-time` | alias of `creation-date` | | `area` | alias of `image-area` |
| `date` | alias of `creation-date` | | `ar` | alias of `image-aspect-ratio` |
| `time` | alias of `creation-date` | | `aspect-ratio` | alias of `image-aspect-ratio` |
| `last-edit-date` | edited at given date | | `creation-date` | posted at given date |
| `last-edit-time` | alias of `last-edit-date` | | `creation-time` | alias of `creation-date` |
| `edit-date` | alias of `last-edit-date` | | `date` | alias of `creation-date` |
| `edit-time` | alias of `last-edit-date` | | `time` | alias of `creation-date` |
| `comment-date` | commented at given date | | `last-edit-date` | edited at given date |
| `comment-time` | alias of `comment-date` | | `last-edit-time` | alias of `last-edit-date` |
| `fav-date` | last favorited at given date | | `edit-date` | alias of `last-edit-date` |
| `fav-time` | alias of `fav-date` | | `edit-time` | alias of `last-edit-date` |
| `feature-date` | featured at given date | | `comment-date` | commented at given date |
| `feature-time` | alias of `feature-time` | | `comment-time` | alias of `comment-date` |
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` (or `questionable`) or `unsafe`. | | `fav-date` | last favorited at given date |
| `rating` | alias of `safety` | | `fav-time` | alias of `fav-date` |
| `feature-date` | featured at given date |
| `feature-time` | alias of `feature-time` |
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` (or `questionable`) or `unsafe`. |
| `rating` | alias of `safety` |
**Sort style tokens** **Sort style tokens**

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]),