-
Notifications
You must be signed in to change notification settings - Fork 52
Added search feature #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f5d87f5
32c3cb6
9919f2e
c6cb120
7349c9e
779e50a
5a572c2
491f62b
e8c448c
6be4ae3
ab8ce55
324a5f3
49e9342
310797e
b389e84
8b3de7e
ef5f82c
ad2ba2f
f2126b5
5491fd8
bf7cd02
2b56c3d
5140285
6542bf2
36ccb5f
810e724
51c09b3
de4a6a9
be648fc
877a072
e7e1d7b
93ca6b6
fec4d9d
c3c1db4
64d49a1
4cc28b6
585afeb
e2c5b06
4fe169a
c6cc17c
e8f067d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -138,4 +138,9 @@ dmypy.json | |
# Pyre type checker | ||
.pyre/ | ||
|
||
|
||
# VScode | ||
.vscode/ | ||
app/.vscode/ | ||
|
||
app/routers/stam |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
from datetime import datetime | ||
|
||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String | ||
from sqlalchemy.orm import relationship | ||
|
||
from app.config import PSQL_ENVIRONMENT | ||
from app.database.database import Base | ||
from sqlalchemy import (DDL, Boolean, Column, DateTime, ForeignKey, Index, | ||
Integer, String, event) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where do you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. event.listen(.....) |
||
from sqlalchemy.dialects.postgresql import TSVECTOR | ||
from sqlalchemy.orm import relationship | ||
|
||
|
||
class UserEvent(Base): | ||
|
@@ -54,10 +56,39 @@ class Event(Base): | |
|
||
participants = relationship("UserEvent", back_populates="events") | ||
|
||
# PostgreSQL | ||
advaa123 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if PSQL_ENVIRONMENT: | ||
events_tsv = Column(TSVECTOR) | ||
__table_args__ = (Index( | ||
'events_tsv_idx', | ||
'events_tsv', | ||
postgresql_using='gin'), | ||
) | ||
|
||
def __repr__(self): | ||
return f'<Event {self.id}>' | ||
|
||
|
||
class PSQLEnvironmentError(Exception): | ||
pass | ||
|
||
|
||
# PostgreSQL | ||
if PSQL_ENVIRONMENT: | ||
trigger_snippet = DDL(""" | ||
CREATE TRIGGER ix_events_tsv_update BEFORE INSERT OR UPDATE | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider save the query content in other file (probably const) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought keeping it there would make it easier for other programmers to understand the code, do you think otherwise? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a good variable name (and maybe a short description) will explain the essence more than the how (the query itself), what do you think? |
||
ON events | ||
FOR EACH ROW EXECUTE PROCEDURE | ||
tsvector_update_trigger(events_tsv,'pg_catalog.english','title','content') | ||
""") | ||
|
||
event.listen( | ||
Event.__table__, | ||
'after_create', | ||
trigger_snippet.execute_if(dialect='postgresql') | ||
) | ||
|
||
|
||
class Invitation(Base): | ||
__tablename__ = "invitations" | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
from typing import List | ||
|
||
from app.database.database import SessionLocal | ||
from app.database.models import Event | ||
from sqlalchemy.exc import SQLAlchemyError | ||
|
||
|
||
def get_stripped_keywords(keywords: str) -> str: | ||
'''Gets a string of keywords to search for from the user form | ||
and returns a stripped ready-to-db-search keywords string''' | ||
|
||
keywords = " ".join(keywords.split()) | ||
keywords = keywords.replace(" ", ":* & ") + ":*" | ||
return keywords | ||
|
||
|
||
def get_results_by_keywords( | ||
session: SessionLocal, | ||
keywords: str, | ||
owner_id: int | ||
) -> List[Event]: | ||
"""Returns possible results for a search in the 'events' database table | ||
|
||
Args: | ||
keywords (str): search string | ||
owner_id (int): current user id | ||
|
||
Returns: | ||
list: a list of events from the database matching the inserted keywords | ||
|
||
Uses PostgreSQL's built in 'Full-text search' feature | ||
(doesn't work with SQLite)""" | ||
|
||
keywords = get_stripped_keywords(keywords) | ||
|
||
try: | ||
return session.query(Event).filter( | ||
Event.owner_id == owner_id, | ||
Event.events_tsv.match(keywords)).all() | ||
|
||
except (SQLAlchemyError, AttributeError): | ||
return [] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
from app.database.database import get_db | ||
from app.dependencies import templates | ||
from app.internal.search import get_results_by_keywords | ||
from fastapi import APIRouter, Depends, Form, Request | ||
from sqlalchemy.orm import Session | ||
|
||
|
||
router = APIRouter() | ||
|
||
|
||
@router.get("/search") | ||
def search(request: Request): | ||
# Made up user details until there's a user login system | ||
current_username = "Chuck Norris" | ||
|
||
return templates.TemplateResponse("search.html", { | ||
"request": request, | ||
"username": current_username | ||
}) | ||
|
||
|
||
@router.post("/search") | ||
async def show_results( | ||
request: Request, | ||
keywords: str = Form(None), | ||
db: Session = Depends(get_db)): | ||
# Made up user details until there's a user login system | ||
current_username = "Chuck Norris" | ||
current_user = 1 | ||
|
||
message = "" | ||
|
||
if not keywords: | ||
message = "Invalid request." | ||
results = None | ||
else: | ||
results = get_results_by_keywords(db, keywords, owner_id=current_user) | ||
if not results: | ||
message = f"No matching results for '{keywords}'." | ||
|
||
return templates.TemplateResponse("search.html", { | ||
"request": request, | ||
"username": current_username, | ||
"message": message, | ||
"results": results, | ||
"keywords": keywords | ||
} | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -246,4 +246,4 @@ <h6 class="card-title text-center mb-1">{{ user.full_name }}</h6> | |
</div> | ||
</div> | ||
|
||
{% endblock %} | ||
{% endblock %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
{% extends "base.html" %} | ||
|
||
|
||
{% block content %} | ||
|
||
<div class="container mt-4"> | ||
<h1>Hello, {{ username }}</h1> | ||
<form name="searchForm" action="/search" onsubmit="return validateForm()" method="POST" required> | ||
<div class="form-row align-items-center"> | ||
<div class="col-sm-2 my-1"> | ||
<label for="keywords" class="sr-only">Search event by keyword</label> | ||
<input type="text" id="keywords" name="keywords" class="form-control" placeholder="Keywords" | ||
value="{{ keywords }}" onfocus="this.value=''" required> | ||
</div> | ||
<div class="col my-1"> | ||
<input type="submit" class="btn btn-primary" value="Search"> | ||
</div> | ||
</form> | ||
</div> | ||
|
||
{% if message %} | ||
<div class="container mt-4"> | ||
{{ message }} | ||
</div> | ||
{% endif %} | ||
|
||
<!-- Results --> | ||
{% if results %} | ||
<div class="container mt-4"> | ||
<div class="my-4"> | ||
Showing results for '{{ keywords }}': | ||
</div> | ||
<!-- Center --> | ||
<div class="col-5"> | ||
<div class="mb-3"> | ||
{% for result in results %} | ||
<!-- Events card --> | ||
<div class="card card-event mb-2 pb-0"> | ||
<div class="card-header d-flex justify-content-between"> | ||
<small> | ||
<i>{{ loop.index }}. {{ result.title }}</i> | ||
</small> | ||
</div> | ||
<div class="card-body pb-1"> | ||
<p class="card-text"> | ||
{{ result.content }} | ||
</p> | ||
<hr class="mb-0"> | ||
<small class="event-posted-time text-muted">{{ result.date }}</small> | ||
</div> | ||
</div> | ||
<!-- End Events card --> | ||
{% endfor %} | ||
</div> | ||
</div> | ||
{% endif %} | ||
|
||
<!-- Form field validation script --> | ||
<script> | ||
function validateForm() { | ||
var x = document.forms["searchForm"]["keywords"].value; | ||
if (x == "") { | ||
alert("Name must be filled out"); | ||
return false; | ||
} | ||
} | ||
</script> | ||
<!-- End script --> | ||
{% endblock %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you want to add it as env value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered it and chose otherwise, thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be happy to hear why :)