Skip to content

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

Merged
merged 41 commits into from
Jan 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f5d87f5
added search feature
advaa123 Jan 20, 2021
32c3cb6
starting merge
advaa123 Jan 20, 2021
9919f2e
help
advaa123 Jan 20, 2021
c6cb120
merging
advaa123 Jan 20, 2021
7349c9e
merging
advaa123 Jan 20, 2021
779e50a
merged with develop
advaa123 Jan 20, 2021
5a572c2
fixes
advaa123 Jan 20, 2021
491f62b
changes for yam
advaa123 Jan 21, 2021
e8c448c
changes for yam2
advaa123 Jan 21, 2021
6be4ae3
changes for yam3
advaa123 Jan 21, 2021
ab8ce55
Merge branch 'develop' into feature/search
yammesicka Jan 22, 2021
324a5f3
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
advaa123 Jan 24, 2021
49e9342
merged develop and feature/search
advaa123 Jan 24, 2021
310797e
merge developinto feature/search
advaa123 Jan 24, 2021
b389e84
merge developinto feature/search
advaa123 Jan 24, 2021
8b3de7e
merge develop into feature/search
advaa123 Jan 24, 2021
ef5f82c
merge develop into feature/search
advaa123 Jan 24, 2021
ad2ba2f
fixed flake8 errors
advaa123 Jan 24, 2021
f2126b5
fixed flake8 errors2
advaa123 Jan 24, 2021
5491fd8
fixed flake8 errors3
advaa123 Jan 24, 2021
bf7cd02
fixed pytest errors
advaa123 Jan 24, 2021
2b56c3d
fixed pytest errors2
advaa123 Jan 24, 2021
5140285
added tests for non-psql env
advaa123 Jan 24, 2021
6542bf2
added tests for non-psql env2
advaa123 Jan 24, 2021
36ccb5f
added tests for non-psql env3
advaa123 Jan 24, 2021
810e724
added tests for non-psql env4
advaa123 Jan 24, 2021
51c09b3
added psql_environment test
advaa123 Jan 24, 2021
de4a6a9
added psql_environment test1
advaa123 Jan 24, 2021
be648fc
added psql_environment test2
advaa123 Jan 24, 2021
877a072
Coverage improvement
advaa123 Jan 24, 2021
e7e1d7b
Coverage improvement1
advaa123 Jan 24, 2021
93ca6b6
Coverage improvement
advaa123 Jan 24, 2021
fec4d9d
Coverage improvement3
advaa123 Jan 24, 2021
c3c1db4
Coverage improvement
advaa123 Jan 24, 2021
64d49a1
Fixes for yam
advaa123 Jan 25, 2021
4cc28b6
Merge branch 'develop' into feature/search
advaa123 Jan 25, 2021
585afeb
Fixes for yam & flake8
advaa123 Jan 25, 2021
e2c5b06
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
advaa123 Jan 25, 2021
4fe169a
Fixes for yam & flake8
advaa123 Jan 25, 2021
c6cc17c
Fixes for yam & flake8
advaa123 Jan 25, 2021
e8f067d
Removed settings.json
advaa123 Jan 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,9 @@ dmypy.json
# Pyre type checker
.pyre/


# VScode
.vscode/
app/.vscode/

app/routers/stam
3 changes: 2 additions & 1 deletion app/config.py.example
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import os

from fastapi_mail import ConnectionConfig

# flake8: noqa

# general
DOMAIN = 'Our-Domain'

# DATABASE
DEVELOPMENT_DATABASE_STRING = "sqlite:///./dev.db"
# Set the following True if working on PSQL environment or set False otherwise
PSQL_ENVIRONMENT = False
Copy link
Contributor

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?

Copy link
Contributor Author

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!

Copy link
Contributor

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 :)


# MEDIA
MEDIA_DIRECTORY = 'media'
Expand Down
14 changes: 11 additions & 3 deletions app/database/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@

from app import config


SQLALCHEMY_DATABASE_URL = os.getenv(
"DATABASE_CONNECTION_STRING", config.DEVELOPMENT_DATABASE_STRING)

engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

def create_env_engine(psql_environment, sqlalchemy_database_url):
if not psql_environment:
return create_engine(
sqlalchemy_database_url, connect_args={"check_same_thread": False})

return create_engine(sqlalchemy_database_url)


engine = create_env_engine(config.PSQL_ENVIRONMENT, SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()
Expand Down
37 changes: 34 additions & 3 deletions app/database/models.py
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do you use event?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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):
Expand Down Expand Up @@ -54,10 +56,39 @@ class Event(Base):

participants = relationship("UserEvent", back_populates="events")

# PostgreSQL
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider save the query content in other file (probably const)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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"

Expand Down
42 changes: 42 additions & 0 deletions app/internal/search.py
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 []
17 changes: 15 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles

from app.config import PSQL_ENVIRONMENT
from app.database import models
from app.database.database import engine
from app.dependencies import (
MEDIA_PATH, STATIC_PATH, templates)
from app.routers import agenda, dayview, event, profile, email, invitation
from app.routers import (agenda, dayview, email, event, invitation, profile,
search)


models.Base.metadata.create_all(bind=engine)
def create_tables(engine, psql_environment):
if 'sqlite' in str(engine.url) and psql_environment:
raise models.PSQLEnvironmentError(
"You're trying to use PSQL features on SQLite env.\n"
"Please set app.config.PSQL_ENVIRONMENT to False "
"and run the app again."
)
else:
models.Base.metadata.create_all(bind=engine)


create_tables(engine, PSQL_ENVIRONMENT)
app = FastAPI()
app.mount("/static", StaticFiles(directory=STATIC_PATH), name="static")
app.mount("/media", StaticFiles(directory=MEDIA_PATH), name="media")
Expand All @@ -20,6 +32,7 @@
app.include_router(dayview.router)
app.include_router(email.router)
app.include_router(invitation.router)
app.include_router(search.router)


@app.get("/")
Expand Down
48 changes: 48 additions & 0 deletions app/routers/search.py
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
}
)
3 changes: 3 additions & 0 deletions app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
<li class="nav-item">
<a class="nav-link" href="{{ url_for('view_invitations') }}">Invitations</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/search">Search</a>
</li>
</ul>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/templates/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,4 @@ <h6 class="card-title text-center mb-1">{{ user.full_name }}</h6>
</div>
</div>

{% endblock %}
{% endblock %}
69 changes: 69 additions & 0 deletions app/templates/search.html
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 %}
Empty file added app/utils/__init__.py
Empty file.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ packaging==20.8
Pillow==8.1.0
pluggy==0.13.1
priority==1.3.0
psycopg2==2.8.6
py==1.10.0
pydantic==1.7.3
pyparsing==2.4.7
Expand Down
41 changes: 36 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import pytest
from app.config import PSQL_ENVIRONMENT
from app.database.database import Base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from app.database.database import Base

pytest_plugins = [
'tests.user_fixture',
Expand All @@ -13,11 +14,25 @@
'smtpdfix',
]

SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db"
# When testing in a PostgreSQL environment please make sure that:
# - Base string is a PSQL string
# - app.config.PSQL_ENVIRONMENT is set to True

if PSQL_ENVIRONMENT:
SQLALCHEMY_TEST_DATABASE_URL = (
"postgresql://postgres:1234"
"@localhost/postgres"
)
test_engine = create_engine(
SQLALCHEMY_TEST_DATABASE_URL
)

else:
SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db"
test_engine = create_engine(
SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False}
)

test_engine = create_engine(
SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False}
)
TestingSessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=test_engine)

Expand All @@ -33,3 +48,19 @@ def session():
yield session
session.close()
Base.metadata.drop_all(bind=test_engine)


@pytest.fixture
def sqlite_engine():
SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db"
sqlite_test_engine = create_engine(
SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False}
)

TestingSession = sessionmaker(
autocommit=False, autoflush=False, bind=sqlite_test_engine)

yield sqlite_test_engine
session = TestingSession()
session.close()
Base.metadata.drop_all(bind=sqlite_test_engine)
Loading