I have written a search function in order to do FTS on my user model however, I am having a hard time figuring out how to configure it in order to get similar results as well, for instance, if the username contains MOST character of the search query it should display it as well. for instance if I search for bob and there is a user with username bobuser13 it should be displayed as well.
Currently, my search function is:
def search(self, search_text):
search_vectors = (
SearchVector(
'username', weight='A', config='english'
) + SearchVector(
'last_name' , weight='B', config='english'
) + SearchVector(
'first_name' , weight='C', config='english'
)+ SearchVector(
'first_name','last_name', weight='D', config='english'
) + + SearchVector(
'bio', weight='D', config='english'
)
)
search_query = SearchQuery(
search_text, config=' english'
)
search_rank = SearchRank(search_vectors,search_query)
trigram = TrigramSimilarity(
'username', search_text
) + TrigramSimilarity(
'last_name',search_text
) + TrigramSimilarity(
'first_name',search_text
)
qs = (
self.get_queryset()
.filter(sv=search_query)
.annotate(rank=search_rank, trigram=trigram, bs=Greatest('rank','trigram'))
.filter(Q(bs__gte=0.1))
.order_by('-bs')
)
return qs
And my user model is:
class Profile(AbstractUser):
bio = models.TextField()
sv = pg_search.SearchVectorField(null=True)
objects = ProfileManager()
class Meta:
indexes = [
GinIndex(fields=['sv'],name='search_idx_user'),
]
def __str__(self):
return self.username
def get_absolute_url(self):
return "{}".format(self.slug)
how can I configure this so that it also includes similar names as well?
Related
Working in Django Rest Framework (DRF), django-filter, and PostgreSQL, and having an issue with one of our endpoints.
Assume the following:
# models.py
class Company(models.Model):
name = models.CharField(max_length=50)
class Venue(models.Model):
company = models.ForeignKey(to="Company", on_delete=models.CASCADE)
name = models.CharField(max_length=50)
# create some data
company1 = Company.objects.create(name="Proper Ltd")
company2 = Company.objects.create(name="MyCompany Ltd")
Venue.objects.create(name="Venue #1", company=company1)
Venue.objects.create(name="Venue #2", company=company1)
Venue.objects.create(name="Property #1", company=company2)
Venue.objects.create(name="Property #2", company=company2)
# viewset
class CompanyViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CompanyVenueSearchSerializer
queryset = (
Venue.objects.all()
.select_related("company")
.order_by("company__name")
)
permission_classes = (ReadOnly,)
http_method_names = ["get"]
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = CompanyVenueListFilter
pagination_class = None
# filterset
class CompanyVenueListFilter(filters.FilterSet):
text = filters.CharFilter(method="name_search")
def name_search(self, qs, name, value):
return qs.filter(
Q(name__icontains=value)
| Q(company__name__icontains=value)
)
class Meta:
model = Venue
fields = [
"name",
"company__name",
]
# serializer
class CompanyVenueSearchSerializer(serializers.ModelSerializer):
company_id = serializers.IntegerField(source="company.pk")
company_name = serializers.CharField(source="company.name")
venue_id = serializers.IntegerField(source="pk")
venue_name = serializers.CharField(source="name")
class Meta:
model = Venue
fields = (
"company_id",
"company_name",
"venue_id",
"venue_name",
)
We now want to allow the user to filter the results by sending a query in the request, e.g. curl -X GET https://example.com/api/company/?text=pr.
The serializer result will look something like:
[
{
"company_id":1,
"company_name":"Proper Ltd",
"venue_id":1,
"venue_name":"Venue #1"
},
{ // update ORM to exclude this dict
"company_id":1,
"company_name":"Proper Ltd",
"venue_id":2,
"venue_name":"Venue #1"
},
{
"company_id":2,
"company_name":"MyCompany Ltd",
"venue_id":3,
"venue_name":"Property #1"
},
{
"company_id":2,
"company_name":"MyCompany Ltd",
"venue_id":4,
"venue_name":"Property #1"
}
]
Expected result:
Want to rewrite the ORM query so that if the filter ("pr") matches the venue__name, return all venues. But if the filter matches the company__name, only return it once, thus in the example above the second dict in the list would be excluded/removed.
Is this possible?
What you can do is to filter Company that matches name filtering and annotate them with the first related Venue and then combine it's results with the second requirement to return venue with name=value
from django.db.models import OuterRef, Q, Subquery
value = "pr"
first_venue = Venue.objects.filter(company__in=OuterRef("id")).order_by("id")
company_qs = Company.objects.filter(name__icontains=value).annotate(
first_venue_id=Subquery(first_venue.values("id")[:1])
)
venue_qs = Venue.objects.filter(
Q(name__icontains=value)
| Q(id__in=company_qs.values_list("first_venue_id", flat=True))
)
The query executed when accessing values of venue_qs looks like
SELECT
"venues_venue"."id",
"venues_venue"."company_id",
"venues_venue"."name"
FROM
"venues_venue"
WHERE
(
UPPER("venues_venue"."name"::TEXT) LIKE UPPER(% pr %)
OR "venues_venue"."id" IN (
SELECT
(
SELECT
U0."id"
FROM
"venues_venue" U0
WHERE
U0."company_id" IN (V0."id")
ORDER BY
U0."id" ASC
LIMIT
1
) AS "first_venue_id"
FROM
"venues_company" V0
WHERE
UPPER(V0."name"::TEXT) LIKE UPPER(% pr %)
)
)
This is how the filter should look like
class CompanyVenueListFilter(filters.FilterSet):
text = filters.CharFilter(method="name_search")
def name_search(self, qs, name, value):
first_venue = Venue.objects.filter(company__in=OuterRef("id")).order_by("id")
company_qs = Company.objects.filter(name__icontains=value).annotate(
first_venue_id=Subquery(first_venue.values("id")[:1])
)
return qs.filter(
Q(name__icontains=value)
| Q(id__in=company_qs.values_list("first_venue_id", flat=True))
)
class Meta:
model = Venue
fields = [
"name",
"company__name",
]
Update for Django 3.2.16
Seems like the query above will not work for such version because it generated a query without parentheses in WHERE clause around V0."id", chunk of query looks like
WHERE
U0."company_id" IN V0."id"
and it makes PostgreSQL complain with error
ERROR: syntax error at or near "V0"
LINE 17: U0."company_id" IN V0."id"
For Django==3.2.16 the filtering method in CompanyVenueListFilter could look like following:
def name_search(self, qs, name, value):
company_qs = Company.objects.filter(name__icontains=value)
venues_qs = (
Venue.objects.filter(company__in=company_qs)
.order_by("company_id", "id")
.distinct("company_id")
)
return qs.filter(Q(name__icontains=value) | Q(id__in=venues_qs.values_list("id")))
The answer is based on other stackoverflow answer and django docs
Django manager annotate first element of m2m as fk
Subquery() expressions
We have a temporary solution, which we're a bit wary about but it seems to do its job. Won't tag this answer as accepted as we're still hoping that someone has a more pythonic/djangoistic solution to the problem.
# viewset
class CompanyViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CompanyVenueSearchSerializer
queryset = (
Venue.objects.all()
.select_related("company")
.order_by("company__name")
)
permission_classes = (ReadOnly,)
http_method_names = ["get"]
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = CompanyVenueListFilter
pagination_class = None
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
text = request.GET.get("text").lower()
first_idx = 0
to_remove = []
for data in serializer.data:
if text in data.get("name").lower() and text not in data.get("venue_name").lower():
if data.get("id") != first_idx:
"""We don't want to remove the first hit of a company whose name matches"""
first_idx = data.get("id")
continue
to_remove.append((data.get("id"), data.get("venue_id")))
return Response(
[
data
for data in serializer.data
if (data.get("id"), data.get("venue_id")) not in to_remove
],
status=status.HTTP_200_OK,
)
class Language(models.Model):
iso_code = models.CharField()
class Publisher(models.Model)
name = models.CharField()
class Book(modle.Model):
name = models.CharField()
language = models.ForeignKey(Language)
publisher = models.ForeignKey(Publisher, related_name='books')
lang_ids = [1,2]
qs = Publisher.objects.annotate(
x=ArrayAgg(
Case(
When(
books__language__in=lang_ids,
then="books__name"
)
)
)
)
I want to filter the qs as shown here - https://docs.djangoproject.com/en/3.1/ref/contrib/postgres/fields/#len
qs.filter(x__len=2)
Why is it impossible to filter the qs this way? I am getting an error IndexError: tuple index out of range.
Output field in ArrayAgg is ArrayField
class ArrayAgg(OrderableAggMixin, Aggregate):
function = 'ARRAY_AGG'
template = '%(function)s(%(distinct)s%(expressions)s %(ordering)s)'
allow_distinct = True
#property
def output_field(self):
return ArrayField(self.source_expressions[0].output_field)
def convert_value(self, value, expression, connection):
if not value:
return []
return value
In order to get length of an ArrayField, you will need to use a different function, there is cardinality and also array_length. More info here https://www.postgresql.org/docs/current/functions-array.html
If you have just one dimenssion array you can use cardinality
from django.db import models
class ArrayLength(models.Func):
function = 'CARDINALITY'
qs = Publisher.objects.annotate(
x=ArrayAgg(
Case(
When(
books__language__in=lang_ids,
then="books__name"
)
)
)
)
qs = qs.annotate(x_len=ArrayLength('x')) # notice `x_len` is just a name, you can use anything
qs = qs.filter(x_len=2) # the actual filter
I'm trying to create an API to upload multiple images per session, I'm using the Django REST Framework. This is the error that I get in Postman:
{
"images": {
"0": {
"non_field_errors": [
"Invalid data. Expected a dictionary, but got InMemoryUploadedFile."
]
}
}
}
models.py with 2 tables
class PostSession(models.Model):
class Meta:
verbose_name = 'session'
verbose_name_plural = 'sessions'
name = models.CharField(
max_length=255,
)
class PostImage(models.Model):
class Meta:
verbose_name = 'photo'
verbose_name_plural = 'photos'
name = models.ForeignKey(
to=PostSession,
on_delete=models.CASCADE,
null=True,
blank=True,
)
file = models.ImageField(
upload_to='photos',
)
serializers.py
class PostImageRetrieveSerializer(serializers.ModelSerializer):
class Meta:
model = PostImage
fields = '__all__'
class PostImageUpdateSerializer(serializers.Serializer):
"""
This class for validate and save child items purpose
"""
name = serializers.CharField(required=False, allow_null=True, allow_blank=True, )
file = serializers.ImageField(required=True, allow_null=False, allow_empty_file=False, )
def create(self, validated_data):
session_name = validated_data.get('name')
image_file = validated_data.get('file')
if session_name and isinstance(session_name, str):
session, _ = PostSession.objects.get_or_create(name=session_name, )
else:
session = None
instance = PostImage.objects.create(
name=session,
file=image_file,
)
return self.update(
instance=instance,
validated_data=validated_data,
)
def update(self, instance, validated_data):
instance.save()
return instance
class PostUploadSerializer(serializers.Serializer):
images = serializers.ListField(
child=PostImageUpdateSerializer(required=True, allow_null=False, many=False, ),
required=True,
allow_null=False,
allow_empty=False,
)
def validate(self, attrs):
images_list = attrs.get('images')
if not isinstance(images_list, list):
raise exceptions.ValidationError(detail={
'images': ['`images` field must be a list of dict object!', ],
})
return attrs
def save_many(self):
images_list = self.validated_data.get('images')
post_image_instances = []
for image_obj in images_list:
try:
post_image_serializer = PostImageSerializer(
context=self.context,
data=image_obj,
many=False,
)
post_image_serializer.is_valid(raise_exception=True, )
post_image = post_image_serializer.save()
post_image_instances.append(post_image)
except:
# TODO: Remove previous saved instances if needed (inside `post_image_instances`)?
pass
return post_image_instances
def create(self, validated_data):
pass
def update(self, instance, validated_data):
pass
viewsets.py
class PostViewSet(viewsets.GenericViewSet):
parser_classes = [parsers.MultiPartParser, parsers.JSONParser, ]
serializer_class = PostImageRetrieveSerializer
queryset = PostImage.objects.all()
#action(methods=['POST', ], detail=False, serializer_class=PostUploadSerializer, )
def upload_images(self, request, *args, **kwargs):
upload_serializer = PostUploadSerializer(
context={'request': request, },
data=request.data,
many=False,
)
upload_serializer.is_valid(raise_exception=True, )
post_image_instances = upload_serializer.save_many()
serializer = self.get_serializer(
post_image_instances,
many=True,
)
return response.Response(
data=serializer.data,
status=status.HTTP_200_OK,
)
The Idea is that the API can create a session with multiple images.
https://gist.github.com/cokelopez/a98ee5569991b6555ecd216764c193ec
I have a model with a ImageField:
class Usr( User ):
idusuario = models.AutoField( primary_key = True )
usuario = models.CharField( blank = False, unique = True, max_length = 50 )
contraseña = models.CharField( blank = False, max_length = 250 )
telefono = models.CharField( max_length = 15, blank = True, null = True )
celular = models.CharField( max_length = 15, blank = True, null = True )
fotografia = models.ImageField( blank = True, null = True, upload_to = 'usuarios' )
depende_de = models.ForeignKey( 'self', on_delete = models.CASCADE, related_name = '+', blank = True, null = True )
def __unicode__( self ):
return self.get_full_name()
def __str__( self ):
return self.get_full_name()
And its form (a ModelForm ):
class RegUsuario( forms.ModelForm ):
class Meta:
model = Usr
fields = [
'usuario',
'contraseña',
'is_active',
'is_superuser',
'first_name',
'last_name',
'email',
'telefono',
'celular',
'fotografia',
'groups',
'depende_de'
]
In views (vw_usuario.py) I have:
#valida_acceso( [ 'usr.agregar_usuarios_usuario' ] )
def new( request ):
if 'POST' == request.method:
frm = RegUsuario( request.POST, files = request.FILES )
if frm.is_valid():
obj = frm.save( commit = False )
obj.username = obj.usuario
obj.set_password( obj.contraseña )
obj.save()
for g in request.POST.getlist( 'groups' ):
obj.groups.add( g )
obj.save()
return HttpResponseRedirect( reverse( 'usuario_ver', kwargs = { 'pk' : obj.pk } ) )
frm = RegUsuario( request.POST or None )
return render( request, 'global/form.html', {
'menu_main' : Usr.objects.filter( id = request.user.pk )[ 0 ].main_menu_struct(),
'footer' : True,
'titulo' : 'Usuarios',
'titulo_descripcion' : 'Nuevo',
'frm' : frm
} )
And in urls.py:
urlpatterns = [
...
path( 'usuarios/nuevo/', vw_usuario.new, name = "usuario_nuevo" ),
...
]
If I go to http://example.com/usuarios/nuevo/ in the browser I can the form displayed as well, then I fill the fields and submit the form, and I got the exception:
Page not found (404)
Request Method: POST
Request URL: http://example.com/usuarios/nuevo/
Raised by: seguridad.mkitsafe.validacion
Using the URLconf defined in example.urls, Django tried these URL patterns, in this order:
...
usuarios/eliminar/<pk>/ [name= usuario_eliminar ]
usuarios/nuevo/ [name= usuario_nuevo ]
usuarios/<pk>/ [name= usuario_ver ]
...
The current path, nuevo/, didn t match any of these.
I don't understand why the url is taken as nuevo/ instead of usuarios/nuevo/
In the form template I'm setting method = "post", enctype="multipart/form-data" and action="" (I've also tried action="usuarios/nuevo" and action="http://example.com/usuarios/nuevo/")
The path for MEDIA_ROOT exists and has enough perms.
The issue happens if I fill in the form the ImageField field or not.
I noticed that if I remove the field in the modelform, everything works as well.
The source code for seguridad.mkitsafe.validacion is:
def valida_acceso( permisos = None ):
url_error = 'seguridad_inicio'
url_autenticacion = 'seguridad_login'
def _valida_acceso( vista ):
def validacion( *args, **kwargs ):
usuario = args[ 0 ].user
if not usuario.is_authenticated:
print_error( "Vista {} negada por autenticación".format( vista.__name__ ), "Exec Info" )
return HttpResponseRedirect( reverse( url_autenticacion ) )
if permisos is None:
return vista( *args, **kwargs )
perms = []
for perm in permisos:
permiso = Permiso.get_from_package_codename( perm )
if( permiso is None ):
print_error( "No se ha encontrado el permiso: " + perm )
else:
perms.append( permiso.perm() )
desc = permiso.descendencia()
for p in desc:
perms.append( p.perm() )
for perm in perms:
p = "{}.{}".format( perm.content_type.app_label, perm.codename )
if usuario.has_perm( p ):
return vista( *args, **kwargs )
print_error( "Vista {} negada por permisos {}".format( vista.__name__, permisos ), "Exec Info" )
return HttpResponseRedirect( reverse( url_error ) )
return validacion
return _valida_acceso
And it is a decorator which verify perms and authentication.
If I remove this decorator the excepion changes in the Raised by to seguridad.vw_usuario.new
Page not found (404)
Request Method: POST
Request URL: http://example.com/usuarios/nuevo/
Raised by: seguridad.vw_usuario.new
Using the URLconf defined in example.urls, Django tried these URL patterns, in this order:
admin/
...
usuarios/actualizar/<pk>/ [name='usuario_actualizar']
usuarios/eliminar/<pk>/ [name='usuario_eliminar']
usuarios/nuevo/ [name='usuario_nuevo']
usuarios/<pk>/ [name='usuario_ver']
usuarios/ [name='usuario_inicio']
...
The current path, nuevo/, didn't match any of these.
I also noticed that when the error is raised and the decorator is active the decorator or the view function is not called.
I also tried to copy a file in MEDIA_ROOT path with an script and it worked as well, so the script has right access to this path.
Any comment about the issue?
I have two models Invoce and Payin. Invoice can be payed in multiple payins. While updating Invoice details with post_save signal. after create logic is executed I attach an Invoice Id to the Payin instance and save it. This save is triggering the signal again(as it's supposed to be) this time update logic i.e in the else part of if create: is executed there by one payin is added twice to the paid amount in invoice. How can I avoid the Update logic when I save instance in the signal logic.
class Invoice(BaseEntity):
""" Invoices are generated based on events state"""
STATUS_CHOICES=(
('CREATED', 'Created'),
('CONFIRMED', 'Confirmed'),
('PARTIAL_PAYMENT', 'Partially Paid'),
('RECEIVED', 'Received'),
('CLOSED', 'Closed')
)
customer = models.ForeignKey(
Customer,
related_name='invoices',
)
event = models.ForeignKey(
'booking.Event',
related_name='invoice',
)
generated_date = models.DateField(
verbose_name='date invoice generated'
)
due_date = models.DateField(
verbose_name='date payment is expected'
)
status = models.CharField(
max_length =15,
choices = STATUS_CHOICES,
default = 'CREATED',
)
amount = models.IntegerField(
default=500,
validators=[
MinValueValidator(
10,
message = 'Amount should be greater than 10'
),
MaxValueValidator(
10000000,
message = 'Amount should be less than 10000000'
),
]
)
paid = models.IntegerField(
default=500,
validators=[
MinValueValidator(
10,
message = 'Amount should be greater than 10'
),
MaxValueValidator(
10000000,
message = 'Amount should be less than 10000000'
),
]
)
def get_absolute_url(self):
return reverse('accounting:Invoice_Detail', kwargs={'pk': self.id})
def get_update_url(self):
return reverse('accounting:Invoice_Update', kwargs={'pk': self.id})
def get_delete_url(self):
return reverse('accounting:Invoice_Delete', kwargs={'pk': self.id})
class Payin(BaseEntity):
""" Payins from all customers"""
MODE_CHOICES=(
('BANK', 'Bank'),
('CHEQUE', 'Cheque'),
('DD', 'Demand Draft'),
('CASH', 'Cash'),
)
customer = models.ForeignKey(
'customers.Customer',
related_name='customer_payins',
blank = True,
null = True,
)
event = models.ForeignKey(
'booking.Event',
related_name='event_payins',
blank = True,
null =True,
)
date = models.DateField(
verbose_name='payment date'
)
time = models.TimeField(
verbose_name='payment time'
)
amount = models.IntegerField(
default=500,
validators=[
MinValueValidator(
10,
message = 'Amount should be greater than 10'
),
MaxValueValidator(
10000000,
message = 'Amount should be less than 10000000'
),
]
)
mode = models.CharField(
max_length =15,
choices = MODE_CHOICES,
default = 'CASH',
)
invoice = models.ForeignKey(
Invoice,
related_name='payins',
blank = True,
null = True,
)
def get_absolute_url(self):
return reverse('accounting:Payin_Detail', kwargs={'id': self.id})
def get_update_url(self):
return reverse('accounting:Payin_Update', kwargs={'id': self.id})
def get_delete_url(self):
return reverse('accounting:Payin_Delete', kwargs={'id': self.id})
#receiver(post_save, sender = Payin)
def update_event_invoice_based_on_payin(sender,instance, created, **kwargs):
payin = instance
#print(dir(instance))
if created:
try:
event = Event.objects.get(pk = payin.event.id )
if event.status != 'COMPLETED':
print('advance before save in post_save in create is: ', event.advance)
event.advance = event.advance + payin.amount
event.save()
print('advance after save in post_save in create is: ' , event.advance)
else:
invoice = Invoice.objects.get(event = event.id)
if invoice.paid + payin.amount <= invoice.amount:
print('post_save before save :',invoice.paid )
invoice.paid = (invoice.paid + payin.amount)
print(invoice.paid)
if invoice.paid == invoice.amount:
invoice.status = 'PAID'
invoice.save()
print('post_save after save in paid :',invoice.paid)
payin.invoice = invoice
print('paid')
payin.save()
else:
invoice.status = 'PARTIAL_PAYMENT'
invoice.save()
print('post_save after save in partial payment :',invoice.paid)
payin.invoice = invoice
print('partial payment')
payin.save()
finally:
#del instance._del
pass
else:
event = Event.objects.get(pk = payin.event.id )
if event.status != 'COMPLETED':
print('advance before save in post_save in modify is: ' ,event.advance)
event.advance = event.advance + payin.amount
event.save()
print('advance after save in post_save in modify is: ' , event.advance)
else:
invoice = Invoice.objects.get(event = event.id)
if invoice.paid + payin.amount <= invoice.amount:
#print(invoice.paid + payin.amount)
print('post_save before save in update :',invoice.paid)
invoice.paid = (invoice.paid + payin.amount)
print(invoice.paid)
if invoice.paid == invoice.amount:
invoice.status = 'PAID'
invoice.save()
print('post_save after save in paid in update :',invoice.paid)
print('paid')
else:
invoice.status = 'PARTIAL_PAYMENT'
invoice.save()
print('post_save after save in partial payment in update :',invoice.paid)
print('partial payment')