Refactor models to inherit from NummiModel and implement custom query sets for enhanced search functionality
This commit is contained in:
parent
d44407d9ab
commit
d5292911c2
9 changed files with 116 additions and 61 deletions
|
@ -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,
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
47
nummi/search/templates/search/search_results.html
Normal file
47
nummi/search/templates/search/search_results.html
Normal 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 %}
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue