refactor: move BackendModel.fuzzy to Backend.fuzzy

This commit is contained in:
Éloi Rivard 2024-04-10 15:52:16 +02:00
parent 8425b2a3b8
commit fa6488bcd1
No known key found for this signature in database
GPG key ID: 7EDA204EA57DD184
10 changed files with 59 additions and 64 deletions

View file

@ -187,7 +187,7 @@ class TableForm(I18NFormMixin, FlaskForm):
filter = filter or {}
super().__init__(**kwargs)
if self.query.data:
self.items = cls.fuzzy(self.query.data, fields, **filter)
self.items = BaseBackend.get().fuzzy(cls, self.query.data, fields, **filter)
else:
self.items = BaseBackend.get().query(cls, **filter)

View file

@ -73,6 +73,11 @@ class BaseBackend:
"""
raise NotImplementedError()
def fuzzy(self, model, query, attributes=None, **kwargs):
"""Works like :meth:`~canaille.backends.BaseBackend.query` but
attribute values loosely be matched."""
raise NotImplementedError()
def check_user_password(self, user, password: str) -> bool:
"""Check if the password matches the user password in the database."""
raise NotImplementedError()

View file

@ -302,6 +302,15 @@ class Backend(BaseBackend):
result = []
return LDAPObjectQuery(model, result)
def fuzzy(self, model, query, attributes=None, **kwargs):
query = ldap.filter.escape_filter_chars(query)
attributes = attributes or model.may() + model.must()
attributes = [model.python_attribute_to_ldap(name) for name in attributes]
filter = (
"(|" + "".join(f"({attribute}=*{query}*)" for attribute in attributes) + ")"
)
return self.query(model, filter=filter, **kwargs)
def setup_ldap_models(config):
from canaille.app import models

View file

@ -240,16 +240,6 @@ class LDAPObject(BackendModel, metaclass=LDAPObjectMetaclass):
return None
@classmethod
def fuzzy(cls, query, attributes=None, **kwargs):
query = ldap.filter.escape_filter_chars(query)
attributes = attributes or cls.may() + cls.must()
attributes = [cls.python_attribute_to_ldap(name) for name in attributes]
filter = (
"(|" + "".join(f"({attribute}=*{query}*)" for attribute in attributes) + ")"
)
return BaseBackend.get().query(cls, filter=filter, **kwargs)
@classmethod
def update_ldap_attributes(cls):
all_object_classes = cls.ldap_object_classes()

View file

@ -65,3 +65,18 @@ class Backend(BaseBackend):
instance._cache = {}
return instances
def fuzzy(self, model, query, attributes=None, **kwargs):
attributes = attributes or model.attributes
instances = self.query(model, **kwargs)
return [
instance
for instance in instances
if any(
query.lower() in value.lower()
for attribute in attributes
for value in model.listify(instance._state.get(attribute, []))
if isinstance(value, str)
)
]

View file

@ -36,22 +36,6 @@ class MemoryModel(BackendModel):
class_name or cls.__name__, {}
).setdefault(attribute, {})
@classmethod
def fuzzy(cls, query, attributes=None, **kwargs):
attributes = attributes or cls.attributes
instances = BaseBackend.get().query(cls, **kwargs)
return [
instance
for instance in instances
if any(
query.lower() in value.lower()
for attribute in attributes
for value in cls.listify(instance._state.get(attribute, []))
if isinstance(value, str)
)
]
@classmethod
def get(cls, identifier=None, /, **kwargs):
if identifier:

View file

@ -87,12 +87,6 @@ class BackendModel:
implemented for every model and for every backend.
"""
@classmethod
def fuzzy(cls, query, attributes=None, **kwargs):
"""Works like :meth:`~canaille.backends.BaseBackend.query` but
attribute values loosely be matched."""
raise NotImplementedError()
@classmethod
def get(cls, identifier=None, **kwargs):
"""Works like :meth:`~canaille.backends.BaseBackend.query` but return

View file

@ -1,4 +1,5 @@
from sqlalchemy import create_engine
from sqlalchemy import or_
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm import declarative_base
@ -78,3 +79,15 @@ class Backend(BaseBackend):
.scalars()
.all()
)
def fuzzy(self, model, query, attributes=None, **kwargs):
attributes = attributes or model.attributes
filter = or_(
getattr(model, attribute_name).ilike(f"%{query}%")
for attribute_name in attributes
if "str" in str(model.attributes[attribute_name])
# erk, photo is an URL string according to SCIM, but bytes here
and attribute_name != "photo"
)
return self.db_session.execute(select(model).filter(filter)).scalars().all()

View file

@ -36,21 +36,6 @@ class SqlAlchemyModel(BackendModel):
f"<{self.__class__.__name__} {self.identifier_attribute}={self.identifier}>"
)
@classmethod
def fuzzy(cls, query, attributes=None, **kwargs):
attributes = attributes or cls.attributes
filter = or_(
getattr(cls, attribute_name).ilike(f"%{query}%")
for attribute_name in attributes
if "str" in str(cls.attributes[attribute_name])
# erk, photo is an URL string according to SCIM, but bytes here
and attribute_name != "photo"
)
return (
Backend.get().db_session.execute(select(cls).filter(filter)).scalars().all()
)
@classmethod
def attribute_filter(cls, name, value):
if isinstance(value, list):

View file

@ -144,28 +144,28 @@ def test_model_indexation(testclient, backend):
def test_fuzzy_unique_attribute(user, moderator, admin, backend):
assert set(backend.query(models.User)) == {user, moderator, admin}
assert set(models.User.fuzzy("Jack")) == {moderator}
assert set(models.User.fuzzy("Jack", ["formatted_name"])) == {moderator}
assert set(models.User.fuzzy("Jack", ["user_name"])) == set()
assert set(models.User.fuzzy("Jack", ["user_name", "formatted_name"])) == {
assert set(backend.fuzzy(models.User, "Jack")) == {moderator}
assert set(backend.fuzzy(models.User, "Jack", ["formatted_name"])) == {moderator}
assert set(backend.fuzzy(models.User, "Jack", ["user_name"])) == set()
assert set(backend.fuzzy(models.User, "Jack", ["user_name", "formatted_name"])) == {
moderator
}
assert set(models.User.fuzzy("moderator")) == {moderator}
assert set(models.User.fuzzy("oderat")) == {moderator}
assert set(models.User.fuzzy("oDeRat")) == {moderator}
assert set(models.User.fuzzy("ack")) == {moderator}
assert set(backend.fuzzy(models.User, "moderator")) == {moderator}
assert set(backend.fuzzy(models.User, "oderat")) == {moderator}
assert set(backend.fuzzy(models.User, "oDeRat")) == {moderator}
assert set(backend.fuzzy(models.User, "ack")) == {moderator}
def test_fuzzy_multiple_attribute(user, moderator, admin, backend):
assert set(backend.query(models.User)) == {user, moderator, admin}
assert set(models.User.fuzzy("jack@doe.com")) == {moderator}
assert set(models.User.fuzzy("jack@doe.com", ["emails"])) == {moderator}
assert set(models.User.fuzzy("jack@doe.com", ["formatted_name"])) == set()
assert set(models.User.fuzzy("jack@doe.com", ["emails", "formatted_name"])) == {
moderator
}
assert set(models.User.fuzzy("ack@doe.co")) == {moderator}
assert set(models.User.fuzzy("doe.com")) == {user, moderator, admin}
assert set(backend.fuzzy(models.User, "jack@doe.com")) == {moderator}
assert set(backend.fuzzy(models.User, "jack@doe.com", ["emails"])) == {moderator}
assert set(backend.fuzzy(models.User, "jack@doe.com", ["formatted_name"])) == set()
assert set(
backend.fuzzy(models.User, "jack@doe.com", ["emails", "formatted_name"])
) == {moderator}
assert set(backend.fuzzy(models.User, "ack@doe.co")) == {moderator}
assert set(backend.fuzzy(models.User, "doe.com")) == {user, moderator, admin}
def test_model_references(testclient, user, foo_group, admin, bar_group, backend):