Skip to content

Commit

Permalink
Merge pull request #46 from ernestofgonzalez/search
Browse files Browse the repository at this point in the history
Add search app
  • Loading branch information
ernestofgonzalez authored Jan 1, 2025
2 parents 3e6dafc + 264fbb1 commit 218c4f2
Show file tree
Hide file tree
Showing 14 changed files with 312 additions and 41 deletions.
9 changes: 9 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
Changelog
=========

.. _v_0_5_0:

0.5.0 (2025-01-01)
------------------

* Added search with OpenSearch
* Added settings for Mixpanel
* Added error logging with Sentry

.. _v_0_4_3:

0.4.3 (2023-08-11)
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = 'Django Rocket'
copyright = '2023, Ernesto F. González'
copyright = '2023-2025, Ernesto F. González'
author = 'Ernesto F. González'
release = '0.4.3'
release = '0.5.0'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
12 changes: 11 additions & 1 deletion {{ cookiecutter.project_slug }}/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@ POSTGRES_DB=
CELERY_BROKER_URL=
CELERY_ACCEPT_CONTENT=

# AWS
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_OPEN_SEARCH_REGION_NAME=
AWS_OPEN_SEARCH_HOST=

# Google
GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
GOOGLE_TAG_ID=

# Heroku
PORT=
Expand All @@ -26,4 +33,7 @@ PORT=
STRIPE_PUBLISHABLE_KEY=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRICE_ID=
STRIPE_PRICE_ID=

# Mixpanel
MIXPANEL_API_TOKEN=
57 changes: 19 additions & 38 deletions {{ cookiecutter.project_slug }}/requirements/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,49 +1,30 @@
django==4.1.4
boto3==1.35.71
celery[redis]==5.4.0
celery==5.4.0
click==8.1.3
django==5.0.6
djangorestframework==3.14.0
django-allauth==0.52.0
djangorestframework-api-key==3.0.0
django-celery-beat==2.6.0
django-cors-headers==3.13.0
django-countries==7.5
dj-database-url==0.5.0
django-jsonform==2.22.0
django-opensearch-dsl==0.6.2
django-phonenumber-field==7.0.0
django-storages==1.13.1
django-debug-toolbar
factory-boy==3.2.1
django-browser-reload==1.6.0
django-compressor==4.1
django-tailwind==3.4.0
django-money==3.0.0

# Dates
pendulum==2.1.2

# CLI
click==8.1.3

# Postgres
psycopg2==2.9.3

# Tasks
celery[redis]==5.2.7
celery==5.2.7
django-celery-beat==2.4.0

# Environment variables
python-dotenv==0.15.0

# WSGI
django-hijack==3.5.3
django-json-widget==2.0.1
dj-database-url==0.5.0
factory-boy==3.2.1
google-auth==2.16.0
gunicorn==20.0.4

# Static files
whitenoise==6.2.0

# Phone numbers
phonenumbers==8.13.0

# Short UUID
psycopg2==2.9.3
python-dotenv==0.15.0
sentry-sdk==2.18.0
serpy==0.3.1
shortuuid==1.0.11

# Google
google-auth==2.16.0

# Billing
stripe==4.2.0
whitenoise==6.2.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.conf import settings
from rest_framework.permissions import SAFE_METHODS, BasePermission
from rest_framework_api_key.models import APIKey
from rest_framework_api_key.permissions import BaseHasAPIKey


class AllowAnyInDebug(BasePermission):
def has_permission(self, request, view):
if settings.DEBUG:
return True
return False


class IsAdminUserAndReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method not in SAFE_METHODS:
return False

return request.user and request.user.is_staff


class IsConsumerAuthenticated(BaseHasAPIKey):
model = APIKey
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.urls import path

from {{cookiecutter.project_slug}}.search import api_views

app_name = "{{ cookiecutter.project_slug }}-search"

urlpatterns = [
path(
"search/",
api_views.search_view,
name="search",
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.status import HTTP_200_OK

from {{cookiecutter.project_slug}}.permissions import IsConsumerAuthenticated
from {{cookiecutter.project_slug}}.search.serializers import SearchHitSerializer
from {{cookiecutter.project_slug}}.search.utils import search


@api_view(["GET"])
@permission_classes([IsConsumerAuthenticated, IsAuthenticated])
def search_view(request):
q = request.GET.get("q", None)

course = request.GET.get("course", None)

s = search(q, course_uuid=course)
hits_serializer = SearchHitSerializer(s.hits, many=True)

return Response(
{
"total": len(hits_serializer.data),
"results": hits_serializer.data,
},
status=HTTP_200_OK,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig


class SearchConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "{{ cookiecutter.project_slug }}.search"
label = "{{ cookiecutter.project_slug }}_search"
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import serpy

from {{cookiecutter.project_slug}}.serializers import Serializer


class SearchHitSerializer(Serializer):
id = serpy.Field()
type = serpy.Field()

def _serialize(self, instance, fields=None):
# Dynamically select the appropriate serializer based on the 'type' field
type_to_serializer = {
# NOTE: include a map from instance.type to respective serializer class
}

serializer_class = type_to_serializer.get(instance["type"])
if not serializer_class:
raise ValueError(f"Unknown type: {instance['type']}")

serialized_data = {
"id": instance["uuid"],
"type": instance["type"],
**serializer_class(instance).data
}

return serialized_data
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django_opensearch_dsl.search import Search


def search(query, course_uuid=None):
search = Search(index="exercises")
search = search.query(
"multi_match",
query=query,
fields=[
"content_name^3",
"content_messages_text^2",
"content_messages_text_translation",
],
fuzziness="AUTO",
)

if course_uuid:
search = search.filter("term", course_uuid=course_uuid)

return search.execute()
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import serpy


class Serializer(serpy.Serializer):
def __init__(self, *args, **kwargs):
fields = kwargs.pop("fields", None)
super(Serializer, self).__init__(*args, **kwargs)

# If fields is passed, includes only passed fields
# in the .to_value() step, allowing to skip query/serialize
# only the necessary fields.
if fields is not None:
allowed = set(fields)
existing = set(self._field_map)
for field_name in existing - allowed:
del self._field_map[field_name]
self._compiled_fields = list(
filter(lambda x: x[0] in allowed, self._compiled_fields)
)

context = kwargs.pop("context", None)
if context is not None:
self.context = context
Loading

0 comments on commit 218c4f2

Please sign in to comment.