Refactor models to inherit from NummiModel and implement custom query sets for enhanced search functionality

This commit is contained in:
Edgar P. Burkhart 2025-01-04 21:57:21 +01:00
parent d44407d9ab
commit d5292911c2
Signed by: edpibu
GPG key ID: 9833D3C5A25BD227
9 changed files with 116 additions and 61 deletions

View file

@ -4,10 +4,10 @@ from django.apps import apps
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from main.models import UserModel from main.models import NummiModel
class Account(UserModel): class Account(NummiModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(max_length=64, default=_("Account"), verbose_name=_("Name")) name = models.CharField(max_length=64, default=_("Account"), verbose_name=_("Name"))
icon = models.SlugField( icon = models.SlugField(
@ -46,7 +46,7 @@ class Account(UserModel):
verbose_name_plural = _("Accounts") verbose_name_plural = _("Accounts")
class AccountModel(UserModel): class AccountModel(NummiModel):
account = models.ForeignKey( account = models.ForeignKey(
Account, Account,
on_delete=models.CASCADE, on_delete=models.CASCADE,

View file

@ -1,7 +1,7 @@
{% load i18n main_extras %} {% load i18n main_extras %}
<dl class="accounts"> <dl class="accounts">
{% for acc in accounts %} {% for acc in accounts %}
<div class="account {% if acc.archived %}archived{% endif %}"> <div class="account {% if not search and acc.archived %}archived{% endif %}">
<dt> <dt>
<a href="{{ acc.get_absolute_url }}">{{ acc.icon|remix }}{{ acc }}</a> <a href="{{ acc.get_absolute_url }}">{{ acc.icon|remix }}{{ acc }}</a>
</dt> </dt>
@ -19,7 +19,7 @@
{{ accounts|balance|value }} {{ accounts|balance|value }}
</dd> </dd>
</div> </div>
{% else %} {% elif not search %}
<div class="more account"> <div class="more account">
<dt> <dt>
<label class="wi" for="show-archived-accounts"> <label class="wi" for="show-archived-accounts">

View file

@ -3,10 +3,10 @@ from uuid import uuid4
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from main.models import UserModel from main.models import NummiModel
class Category(UserModel): class Category(NummiModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField( name = models.CharField(
max_length=64, default=_("Category"), verbose_name=_("Name") max_length=64, default=_("Category"), verbose_name=_("Name")

View file

@ -1,15 +1,46 @@
from django.conf import settings from django.conf import settings
from django.contrib.postgres.search import (
SearchQuery,
SearchRank,
SearchVector,
TrigramSimilarity,
)
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class UserModel(models.Model): class NummiQuerySet(models.QuerySet):
main_field = "name"
fields = dict()
def search(self, search):
return (
self.annotate(
rank=SearchRank(
sum(
(
SearchVector(field, weight=weight)
for field, weight in self.fields.items()
),
start=SearchVector(self.main_field, weight="A"),
),
SearchQuery(search, search_type="websearch"),
),
similarity=TrigramSimilarity("name", search),
)
.filter(models.Q(rank__gte=0.1) | models.Q(similarity__gte=0.3))
.order_by("-rank")
)
class NummiModel(models.Model):
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_("User"), verbose_name=_("User"),
editable=False, editable=False,
) )
objects = NummiQuerySet.as_manager()
class Meta: class Meta:
abstract = True abstract = True

View file

@ -1,6 +1,7 @@
{% extends "main/form/form_base.html" %} {% extends "main/form/form_base.html" %}
{% load i18n %} {% load i18n %}
{% block buttons %} {% block buttons %}
<input type="reset" />
<input type="submit" value="{% translate "Search" %}" /> <input type="submit" value="{% translate "Search" %}" />
{% endblock %} <input type="reset" value="{% translate "Reset" %}" />
<a href="{% url "search" %}">{% translate "Clear" %}</a>
{% endblock buttons %}

View file

@ -0,0 +1,47 @@
{% extends "main/base.html" %}
{% load i18n static %}
{% load main_extras account_extras transaction_extras %}
{% block title %}
{% translate "Search" %} Nummi
{% endblock title %}
{% block link %}
{{ block.super }}
{% css "main/css/form.css" %}
{% css "main/css/table.css" %}
{% endblock link %}
{% block body %}
<h2>{% translate "Search" %}</h2>
<form method="post" action="{% url "search" %}">
{% csrf_token %}
{{ form }}
</form>
{% if accounts %}
<section>
<h3>{% translate "Accounts" %}</h3>
<div class="split">{% account_table accounts search=True %}</div>
</section>
{% endif %}
{% if categories %}
<section>
<h3>{% translate "Categories" %}</h3>
<p>
{% for cat in categories %}
<a class="category" href="{{ cat.get_absolute_url }}">{{ cat.icon|remix }}{{ cat }}</a>
{% endfor %}
</p>
</section>
{% endif %}
{% if transactions %}
<section>
<h3>{% translate "Transactions" %}</h3>
{% url_get "transactions" search=search as t_url %}
<p>
<a class="big-link" href="{{ t_url }}">{{ "list-check"|remixnl }}{% translate "Show all transactions" %}</a>
</p>
{% transaction_table transactions n_max=8 transactions_url=t_url %}
</section>
{% endif %}
{% if not accounts and not categories and not transactions %}
<p>{% translate "No results found." %}</p>
{% endif %}
{% endblock body %}

View file

@ -1,14 +1,7 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.postgres.search import (
SearchQuery,
SearchRank,
SearchVector,
TrigramSimilarity,
)
from django.db import models
from django.shortcuts import redirect from django.shortcuts import redirect
from django.views.generic import TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from transaction.views import TransactionListView
from .forms import SearchForm from .forms import SearchForm
@ -21,24 +14,17 @@ class SearchFormView(LoginRequiredMixin, FormView):
return redirect("search", search=form.cleaned_data.get("search")) return redirect("search", search=form.cleaned_data.get("search"))
class SearchView(TransactionListView): class SearchView(LoginRequiredMixin, TemplateView):
def get_queryset(self): template_name = "search/search_results.html"
self.search = self.kwargs["search"]
return (
super()
.get_queryset()
.annotate(
rank=SearchRank(
SearchVector("name", weight="A")
+ SearchVector("description", weight="B")
+ SearchVector("trader", weight="B"),
SearchQuery(self.search, search_type="websearch"),
),
similarity=TrigramSimilarity("name", self.search),
)
.filter(models.Q(rank__gte=0.1) | models.Q(similarity__gte=0.3))
.order_by("-rank", "-date")
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {"search": self.kwargs["search"]} context = super().get_context_data(**kwargs)
_user = self.request.user
context["form"] = SearchForm(initial={"search": self.kwargs["search"]})
context["search"] = self.kwargs["search"]
context["transactions"] = _user.transaction_set.search(self.kwargs["search"])
context["accounts"] = _user.account_set.search(self.kwargs["search"])
context["categories"] = _user.category_set.search(self.kwargs["search"])
return context

View file

@ -7,12 +7,20 @@ from django.core.validators import FileExtensionValidator
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from main.models import UserModel from main.models import NummiModel, NummiQuerySet
from media.utils import get_path from media.utils import get_path
from statement.models import Statement from statement.models import Statement
class Transaction(UserModel): class TransactionQuerySet(NummiQuerySet):
fields = {
"description": "B",
"trader": "B",
"category__name": "C",
}
class Transaction(NummiModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField( name = models.CharField(
max_length=256, default=_("Transaction"), verbose_name=_("Name") max_length=256, default=_("Transaction"), verbose_name=_("Name")
@ -44,6 +52,8 @@ class Transaction(UserModel):
verbose_name=_("Statement"), verbose_name=_("Statement"),
) )
objects = TransactionQuerySet.as_manager()
def __str__(self): def __str__(self):
return f"{self.name}" return f"{self.name}"
@ -64,7 +74,7 @@ class Transaction(UserModel):
verbose_name_plural = _("Transactions") verbose_name_plural = _("Transactions")
class Invoice(UserModel): class Invoice(NummiModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField( name = models.CharField(
max_length=256, default=_("Invoice"), verbose_name=_("Name") max_length=256, default=_("Invoice"), verbose_name=_("Name")

View file

@ -1,13 +1,6 @@
from account.models import Account from account.models import Account
from category.models import Category from category.models import Category
from django.contrib import messages from django.contrib import messages
from django.contrib.postgres.search import (
SearchQuery,
SearchRank,
SearchVector,
TrigramSimilarity,
)
from django.db import models
from django.forms import ValidationError from django.forms import ValidationError
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -193,20 +186,7 @@ class TransactionListView(NummiListView):
if statement := self.request.GET.get("statement"): if statement := self.request.GET.get("statement"):
queryset = queryset.filter(statement=statement) queryset = queryset.filter(statement=statement)
if search := self.request.GET.get("search"): if search := self.request.GET.get("search"):
queryset = ( queryset = queryset.search(search)
queryset.annotate(
rank=SearchRank(
SearchVector("name", weight="A")
+ SearchVector("description", weight="B")
+ SearchVector("trader", weight="B")
+ SearchVector("category__name", weight="C"),
SearchQuery(search, search_type="websearch"),
),
similarity=TrigramSimilarity("name", search),
)
.filter(models.Q(rank__gte=0.1) | models.Q(similarity__gte=0.3))
.order_by("-rank", "-date")
)
if sort_by := self.request.GET.get("sort_by"): if sort_by := self.request.GET.get("sort_by"):
queryset = queryset.order_by(sort_by) queryset = queryset.order_by(sort_by)