Skip to content
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

feat: filter search results #50

Merged
merged 5 commits into from
Oct 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 0 additions & 3 deletions .github/workflows/api-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ jobs:
- name: Run Flake8 checks
run: poetry run flake8 --count .
working-directory: api
- name: Run MyPy checks
run: poetry run mypy .
working-directory: api

test:
name: Tests
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pipeline-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ env:

jobs:
linter:
name: Mypy, Black and Flake8 Linters
name: Linters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/scheduler-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ env:

jobs:
linter:
name: Mypy, Black and Flake8 Linters
name: Linters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand Down
1,097 changes: 602 additions & 495 deletions api/poetry.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ authors = ["Gilson Filho <[email protected]>"]

[tool.poetry.dependencies]
python = "^3.10"
fastapi = "^0.85.0"
fastapi = "^0.103.2"
pydantic = "^1.10.2"
uvicorn = "^0.18.3"
alembic = "^1.8.1"
Expand All @@ -15,6 +15,7 @@ psycopg2-binary = "^2.9.4"
sqlalchemy-utils = "^0.39.0"
fastapi-pagination = "^0.11.3"
sentry-sdk = {extras = ["fastapi"], version = "^1.32.0"}
sqlalchemy = "1.4.44"

[tool.poetry.dev-dependencies]
pytest = "^7.1.3"
Expand All @@ -26,6 +27,9 @@ types-requests = "^2.28.11"
SQLAlchemy = { extras = ["mypy"], version = "^1.4.41" }
freezegun = "^1.2.2"

[tool.poetry.group.dev.dependencies]
httpx = "^0.25.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
13 changes: 10 additions & 3 deletions api/src/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,17 @@ def index() -> Any:
description="Get list of domains data related by opendata repository",
)
def fetch_domains(
db: Session = Depends(get_db), search: Optional[str] = None
db: Session = Depends(get_db),
search: Optional[str] = None,
available: Optional[bool] = None,
status: Optional[str] = None,
) -> Any: # noqa

if search:
return paginate(DomainService(db).search(search=search))
return paginate(
DomainService(db).search(
{"available": available, "status": status}, search=search
)
)

return paginate(DomainService(db).fetch())
return paginate(DomainService(db).fetch({"available": available, "status": status}))
32 changes: 25 additions & 7 deletions api/src/services.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Dict

from sqlalchemy import or_
from sqlalchemy.orm import Query
from sqlalchemy.orm import Session
from src.models import Domain
Expand All @@ -7,15 +10,30 @@ class DomainService:
def __init__(self, db: Session) -> None:
self.db = db

def fetch(self) -> Query:
return self.db.query(Domain).order_by(Domain.domain)
def fetch(self, filters: Dict[str, str]) -> Query:
query = self.db.query(Domain)

query = query.filter(
or_(Domain.available == filters["available"], filters["available"] is None),
or_(Domain.status.any(filters["status"]), filters["status"] is None),
).order_by(Domain.domain)

return query

def search(self, search: str = "") -> Query:
def search(
self,
filters: Dict[str, str],
search: str = "",
) -> Query:
query = self.db.query(Domain)

result = query.filter(
(Domain.domain.ilike(f"%{search}%"))
| (Domain.organization.ilike(f"%{search}%"))
query = query.filter(
or_(
Domain.domain.ilike(f"%{search}%"),
Domain.organization.ilike(f"%{search}%"),
),
or_(Domain.available == filters["available"], filters["available"] is None),
or_(Domain.status.any(filters["status"]), filters["status"] is None),
).order_by(Domain.domain)

return result
return query
34 changes: 34 additions & 0 deletions api/tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json

import pytest
from freezegun import freeze_time
from src import __version__
from src.core.config import settings
Expand All @@ -26,6 +27,39 @@ def test_domains_endpoint(client, single_domain_schema):
}


@freeze_time("2022-10-14 14:00:00")
@pytest.mark.parametrize(
"available, expected_total, pages", [(True, 1, 1), (False, 0, 0)]
)
def test_domains_with_filter_available(
client, single_domain_schema, available, expected_total, pages
):
resp = client.get(f"/domains?available={available}")

assert resp.status_code == 200
assert resp.json() == {
"items": [json.loads(single_domain_schema.json())] if available else [],
"total": expected_total,
"page": 1,
"pages": pages,
"size": 50,
}


@freeze_time("2022-10-14 14:00:00")
def test_domains_with_filter_status(client, single_domain_schema):
resp = client.get("/domains?status=active")

assert resp.status_code == 200
assert resp.json() == {
"items": [json.loads(single_domain_schema.json())],
"total": 1,
"page": 1,
"pages": 1,
"size": 50,
}


@freeze_time("2022-10-14 14:00:00")
def test_search_domains_endpoint(client, single_domain_schema):
resp = client.get("/domains?search=gdf")
Expand Down
12 changes: 10 additions & 2 deletions api/tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

def test_fetch_domains(db_session, single_domain):
service = DomainService(db_session)
domains = service.fetch().all()
filters = {
"available": None,
"status": None,
}
domains = service.fetch(filters).all()

assert len(domains) > 0
assert domains[0].domain == single_domain.domain
Expand All @@ -12,6 +16,10 @@ def test_fetch_domains(db_session, single_domain):

def test_search_domains(db_session, single_domain):
service = DomainService(db_session)
domains = service.search("gdf").all()
filters = {
"available": None,
"status": None,
}
domains = service.search(filters, search="gdf").all()

assert len(domains) > 0
8 changes: 8 additions & 0 deletions frontend/src/app/api/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ export async function GET(request: NextRequest) {
Number(searchParams.get("page")) || process.env.PAGINATION_INITIAL_PAGE;
const size = process.env.PAGINATION_SIZE;
const search = searchParams.get("search");
const available = searchParams.get("available");
const status = searchParams.get("status");

let baseUrl = `${process.env.CHECKSTATUS_API_URL}/domains?page=${page}&size=${size}`;
if (search) {
baseUrl += `&search=${search}`;
}
if (available) {
baseUrl += `&available=${available}`;
}
if (status) {
baseUrl += `&status=${status}`;
}

const response = await fetch(baseUrl, {
headers: {
Expand Down
42 changes: 28 additions & 14 deletions frontend/src/app/context.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React, { createContext } from "react";
import { DomainsResponse } from "../utils/types";
import { DomainsResponse, FilterSearch } from "../utils/types";
import { getData, searchData } from "../utils";
import { useDebounce } from "usehooks-ts";
import { useRouter } from "next/navigation";

export type DomainContextType = {
getDomains: (page: number) => void;
searchDomains: (search: string, page: number) => void;
getDomains: (page: number, filters: FilterSearch) => void;
searchDomains: (search: string, page: number, filter: FilterSearch) => void;
setLoading: (isLoading: boolean) => void;
setPage: (page: number) => void;
setSearchTerm: (searchTerm: string) => void;
searchTerm: string;
page: number;
setFilter: (filter: FilterSearch) => void;
isLoading: boolean;
page: number;
searchTerm: string;
filter: FilterSearch;
domains: DomainsResponse;
};

Expand All @@ -30,9 +32,11 @@ export const DomainContext = createContext<DomainContextType>({
setLoading: () => {},
setPage: () => {},
setSearchTerm: () => {},
setFilter: () => {},
page: 1,
searchTerm: "",
isLoading: true,
filter: { available: "", status: "" },
domains: defaultDomainsValue,
});

Expand All @@ -42,41 +46,49 @@ const DomainProvider = ({ children }: { children: React.ReactNode }) => {
const [isLoading, setLoading] = React.useState(true);
const [page, setPage] = React.useState<number>(1);
const [searchTerm, setSearchTerm] = React.useState<string>("");
const [filter, setFilter] = React.useState<FilterSearch>({
available: "",
status: "",
});
const [domains, setDomains] =
React.useState<DomainsResponse>(defaultDomainsValue);

const debounceValue = useDebounce<string>(searchTerm, 500);

const getDomains = async (page: number = 1) => {
const getDomains = async (page: number = 1, filters: FilterSearch) => {
setLoading(true);
const response = await getData(page);
const response = await getData(page, filters);

setDomains(response);
setLoading(false);
};

const searchDomains = async (search: string, page: number = 1) => {
const searchDomains = async (
search: string,
page: number = 1,
filter: FilterSearch
) => {
setLoading(true);
const response = await searchData(search, page);
const response = await searchData(search, page, filter);

setDomains(response);
setLoading(false);
};

React.useEffect(() => {
if (searchTerm) {
searchDomains(searchTerm, page);
searchDomains(searchTerm, page, filter);
} else {
getDomains(page);
getDomains(page, filter);
}
}, [page]); //eslint-disable-line
}, [page, filter]); //eslint-disable-line

React.useEffect(() => {
if (searchTerm) {
searchDomains(searchTerm, page);
searchDomains(searchTerm, page, filter);
} else {
setPage(1);
getDomains(page);
getDomains(page, filter);
}
}, [debounceValue]); // eslint-disable-line

Expand All @@ -98,6 +110,8 @@ const DomainProvider = ({ children }: { children: React.ReactNode }) => {
page,
setSearchTerm,
searchTerm,
setFilter,
filter,
}}
>
{children}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="pt-BR">
<body
className={`${font.className} bg-zinc-50 grid grid-cols-1 items-center px-2 sm:px-5 md:px-16 pt-8`}
className={`${font.className} bg-zinc-50 grid grid-cols-1 items-center gap-x-10 px-2 sm:px-5 md:px-16 pt-8`}
>
<GAnalytics />
{children}
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import DomainProvider from "./context";

import React from "react";
import Navbar from "@/components/navbar";
import DomainsFilters from "@/components/domains-filters";

export default function Home() {
return (
<DomainProvider>
<Navbar />
<Header />
<Domains />
<div>
<DomainsFilters />
<Domains />
</div>
</DomainProvider>
);
}
Loading