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
nummi

View file

@ -4,10 +4,10 @@ from django.apps import apps
from django.db import models
from django.urls import reverse
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)
name = models.CharField(max_length=64, default=_("Account"), verbose_name=_("Name"))
icon = models.SlugField(
@ -46,7 +46,7 @@ class Account(UserModel):
verbose_name_plural = _("Accounts")
class AccountModel(UserModel):
class AccountModel(NummiModel):
account = models.ForeignKey(
Account,
on_delete=models.CASCADE,

View file

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

View file

@ -3,10 +3,10 @@ from uuid import uuid4
from django.db import models
from django.urls import reverse
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)
name = models.CharField(
max_length=64, default=_("Category"), verbose_name=_("Name")

View file

@ -1,15 +1,46 @@
from django.conf import settings
from django.contrib.postgres.search import (
SearchQuery,
SearchRank,
SearchVector,
TrigramSimilarity,
)
from django.db import models
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(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name=_("User"),
editable=False,
)
objects = NummiQuerySet.as_manager()
class Meta:
abstract = True

View file

@ -1,6 +1,7 @@
{% extends "main/form/form_base.html" %}
{% load i18n %}
{% block buttons %}
<input type="reset" />
<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.postgres.search import (
SearchQuery,
SearchRank,
SearchVector,
TrigramSimilarity,
)
from django.db import models
from django.shortcuts import redirect
from django.views.generic import TemplateView
from django.views.generic.edit import FormView
from transaction.views import TransactionListView
from .forms import SearchForm
@ -21,24 +14,17 @@ class SearchFormView(LoginRequiredMixin, FormView):
return redirect("search", search=form.cleaned_data.get("search"))
class SearchView(TransactionListView):
def get_queryset(self):
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")
)
class SearchView(LoginRequiredMixin, TemplateView):
template_name = "search/search_results.html"
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.urls import reverse
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 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)
name = models.CharField(
max_length=256, default=_("Transaction"), verbose_name=_("Name")
@ -44,6 +52,8 @@ class Transaction(UserModel):
verbose_name=_("Statement"),
)
objects = TransactionQuerySet.as_manager()
def __str__(self):
return f"{self.name}"
@ -64,7 +74,7 @@ class Transaction(UserModel):
verbose_name_plural = _("Transactions")
class Invoice(UserModel):
class Invoice(NummiModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(
max_length=256, default=_("Invoice"), verbose_name=_("Name")

View file

@ -1,13 +1,6 @@
from account.models import Account
from category.models import Category
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.shortcuts import get_object_or_404
from django.urls import reverse_lazy
@ -193,20 +186,7 @@ class TransactionListView(NummiListView):
if statement := self.request.GET.get("statement"):
queryset = queryset.filter(statement=statement)
if search := self.request.GET.get("search"):
queryset = (
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")
)
queryset = queryset.search(search)
if sort_by := self.request.GET.get("sort_by"):
queryset = queryset.order_by(sort_by)