Skip to content

Commit

Permalink
Merge pull request #15 from likes-gay/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
YummyBacon5 authored Feb 2, 2024
2 parents 6e3a904 + 755c6b9 commit abdb229
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 107 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The easiest and most secure way to run this is using our [offcial Docker image](
* The name argument sets the name

```shell
docker run -e SECRET_KEY="SET_SECRECT_KEY_HERE" --publish 8000:8000 --volume $(pwd)/dict-data:/backend/dict-data --detach --restart always --name Dict likesgay/dict
docker run -e SECRET_KEY="SET_SECRET_KEY_HERE" --publish 8000:8000 --volume $(pwd)/dict-data:/backend/dict-data --detach --restart always --name Dict likesgay/dict
```
The Docker container can automatically be updated to the latest image using [Watchtower](https://containrrr.dev/watchtower/).

Expand All @@ -47,6 +47,8 @@ The Docker container can automatically be updated to the latest image using [Wat
1. Run `npm run build:dev` in [`frontend`](https://github.com/likes-gay/dict/tree/main/frontend)
2. Run `uvicorn main:app --reload` in [`backend`](https://github.com/likes-gay/dict/tree/main/backend)

We also have a [dev branch](https://github.com/likes-gay/dict/tree/dev).

## To Do

- [x] Add updoot button
Expand All @@ -70,4 +72,4 @@ The Docker container can automatically be updated to the latest image using [Wat
- [ ] Add word catergories
- [ ] Add a way to view entries by word catergory
- [ ] Add validation on acepted query params more effeciently
- [ ] Improve frontend tooltip
- [ ] Improve frontend tooltip
184 changes: 108 additions & 76 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@

from random_word import RandomWords

from tinydb import TinyDB, Query
from tinydb import TinyDB, where
from tinydb.operations import increment, decrement

# -------------------------------------------

load_dotenv()

db = TinyDB("dict-data/dict_db.json", create_dirs=True)
app = FastAPI()
app = FastAPI(
title="Dict Backend",
summary="Backend for the Dict project",
version="2.0.0",
)

app.add_middleware(
CORSMiddleware,
Expand All @@ -31,51 +35,67 @@
)

# -------------------------------------------

# Add the new keys into the database. So old data doesn't break everything
db.update({"downdoots": 0}, ~ Query().downdoots.exists())
db.update({"isRobot": False}, ~ Query().isRobot.exists())

# -------------------------------------------
# Input models

class UpdootEnum(str, Enum):
UP = "up"
DOWN = "down"
NONE = "none"
UP = "up"
DOWN = "down"
NONE = "none"

class UpdateUpdoot(BaseModel):
id: int
updootState: UpdootEnum
prevUpdootState: UpdootEnum
id: int
updootState: UpdootEnum
prevUpdootState: UpdootEnum

class UploadWordFormat(BaseModel):
word: str
description: str
creationDate: int
uploader: str
isRobot: bool
word: str
description: str
creationDate: int
uploader: str
isRobot: bool

class DeleteWord(BaseModel):
id: int
secretKey: str
id: int
secretKey: str

class GetWordParam(BaseModel):
word_id: int

class Uploader(BaseModel):
uploader: str
word_id: int

class DirEnum(str, Enum):
DESC = "desc"
ASC = "asc"
DESC = "desc"
ASC = "asc"

class SortByEnum(str, Enum):
TOTALDOOTS = "totaldoots"
UPDOOTS = "updoots"
DOWNDOOTS = "downdoots"
ID = "id"
CREATION_DATE = "date"
ALPHABETICAL = "alphabet"
TOTALDOOTS = "totaldoots"
UPDOOTS = "updoots"
DOWNDOOTS = "downdoots"
ID = "id"
DATE = "date"
ALPHABETICAL = "alphabet"

# -------------------------------------------
# Output models

class Count(BaseModel):
count: int

class Record(BaseModel):
id: int
word: str
description: str
creationDate: int
uploader: str
updoots: int
downdoots: int
isRobot: bool

class RandomWord(BaseModel):
word: Record
realRandomWord: str

class RangeOfWords(BaseModel):
dictWords: list[Record]
max: int

# -------------------------------------------

Expand All @@ -91,15 +111,15 @@ async def remove_trailing_slash(request: Request, call_next):

@app.get("/api", status_code=418)
async def check_if_api_is_working():
return {"I'm a": "teapot"}
pass


@app.get("/api/num_of_words")
@app.get("/api/num_of_words", response_model=Count)
async def count_of_words():
return {"totalWords": len(db)}
return {"count" : len(db)}


@app.post("/api/upload_word", status_code=201)
@app.post("/api/upload_word", response_model=Record, status_code=201)
async def upload_a_new_word(new_word: UploadWordFormat):
# Trim string values
word = new_word.word.strip().lstrip()
Expand All @@ -108,24 +128,24 @@ async def upload_a_new_word(new_word: UploadWordFormat):

# Check if word is empty
if word == "":
raise HTTPException(status_code=400, detail="Word after trimming of white space cannot be empty")
raise HTTPException(status_code=400, detail="Word after trimming white space can't be empty")

# Check if word already exists
if db.search(Query().word == word):
if db.search(where("word") == word):
raise HTTPException(status_code=400, detail="Word already exists")

if description == "":
raise HTTPException(status_code=400, detail="Description cannot be empty")

record = {
"id": len(db),
"id": (max(db.all(), key=lambda x: x["id"])["id"] + 1) or 0,
"word": word,
"description": description,
"creationDate": new_word.creationDate or int(time.time()),
"uploader": uploader or "Unknown",
"updoots": 0,
"downdoots": 0,
"isRobot": False,
"isRobot": new_word.isRobot or False,
}

db.insert(record)
Expand All @@ -138,69 +158,71 @@ async def delete_a_word(req: DeleteWord):
raise HTTPException(status_code=403, detail="Unauthorised, invalid secret key")

# Check if the word exists
word = db.get(Query().id == req.id)
word = db.get(where("id") == req.id)
if not word:
raise HTTPException(status_code=404, detail="Word does not exist")

# Delete word from database
db.remove(doc_ids=[word.doc_id])

@app.post("/api/update_updoot")
@app.post("/api/update_updoot", response_model=Record)
async def update_words_updoot_count(req: UpdateUpdoot):
word_id = req.id

if not db.contains(Query().id == word_id):
if not db.contains(where("id") == word_id):
raise HTTPException(status_code=404, detail="Word does not exist")

if req.prevUpdootState == req.updootState:
raise HTTPException(status_code=400, detail="Cannot update the same updoot state")

if req.prevUpdootState != UpdootEnum.NONE:
db.update(decrement(req.prevUpdootState.value + "doots"), Query().id == word_id)
db.update(decrement(req.prevUpdootState.value + "doots"), where("id") == word_id)

if req.updootState != UpdootEnum.NONE:
db.update(increment(req.updootState.value + "doots"), Query().id == word_id)

return db.get(Query().id == word_id)

db.update(increment(req.updootState.value + "doots"), where("id") == word_id)

@app.get("/api/get_word")
async def get_word_by_ID(wordId: GetWordParam):
response = db.search(Query().id == wordId)

if response:
return response[0]
return db.get(where("id") == word_id)

else:
raise HTTPException(status_code=404, detail="Item not found lol")


@app.get("/api/get_all_words")
@app.get("/api/get_all_words", response_model=list[Record])
async def get_all_words(
sortby: SortByEnum = SortByEnum.UPDOOTS, orderby: DirEnum = DirEnum.DESC
sortby: SortByEnum = SortByEnum.UPDOOTS, orderby: DirEnum = DirEnum.ASC
):
IS_REVERSED = orderby == DirEnum.ASC

# By default, Python sorts from lowest to highest
# So for asending to work (highest to lowest), it needs to be reversed

if sortby == SortByEnum.TOTALDOOTS:
return sorted(db.all(), key=lambda x: x["updoots"] - x["downdoots"], reverse=IS_REVERSED)

elif sortby == SortByEnum.ID:
return sorted(db.all(), key=lambda x: x["id"], reverse=IS_REVERSED)

elif sortby == SortByEnum.UPDOOTS:
print("UPDOOT")
return sorted(db.all(), key=lambda x: x["updoots"], reverse=IS_REVERSED)

elif sortby == SortByEnum.DOWNDOOTS:
return sorted(db.all(), key=lambda x: x["downdoots"], reverse=IS_REVERSED)

elif sortby == SortByEnum.CREATION_DATE:
elif sortby == SortByEnum.DATE:
return sorted(db.all(), key=lambda x: x["creationDate"], reverse=IS_REVERSED)

elif sortby == SortByEnum.ALPHABETICAL:
return sorted(db.all(), key=lambda x: x["word"].lower(), reverse=IS_REVERSED)

@app.get("/api/get_range_of_words")

@app.get("/api/get_word/{wordID}", response_model=Record)
async def get_word_by_ID(wordID: int):
response = db.search(where("id") == wordID)

if response:
return response[0]

else:
raise HTTPException(status_code=404, detail="Item not found lol")


@app.get("/api/get_range_of_words", response_model=RangeOfWords)
async def get_range_of_words(offset: int = 0, size: int = 5):
data = db.all()

Expand All @@ -218,30 +240,40 @@ async def get_range_of_words(offset: int = 0, size: int = 5):
return {"dictWords": data[offset : offset + size], "max": len(db)}


@app.get("/api/lookup_id")
async def lookup_id_of_word(wordId: GetWordParam):
response = db.search(Query().word == wordId)
@app.get("/api/lookup_word/{word}", response_model=Record)
async def lookup_word_by_string(word: str):
response = db.search(where("word") == word)

if response:
return {"id": response[0]["id"]}
return response[0]

raise HTTPException(status_code=404, detail="Word not found lol")


@app.get("/api/get_uploaders_posts/{uploader}", response_model=list[Record])
async def get_all_of_a_uploaders_posts(uploader):
return db.search(where("uploader") == uploader.capitalize())

raise HTTPException(status_code=404, detail="Item not found lol")

@app.get("/api/get_all_uploaders", response_model=list[str])
async def get_names_of_all_uploaders():
arr = list(set(
[record["uploader"] for record in db.search(where("uploader") != "Unknown")]
))

@app.get("/api/get_uploaders_posts")
async def get_all_of_a_uploaders_posts(uploader: Uploader):
response = db.search(Query().uploader == uploader.capitalize())
arr += [record["uploader"] for record in db.search(where("uploader") == "Unknown")]

return response
return arr


@app.get("/api/get_random_word")
@app.get("/api/get_random_word", response_model=RandomWord)
async def get_a_random_word_and_one_from_the_english_dictionary():
return {
"word": choice(db.all())["word"],
"word": choice(db.all()),
"realRandomWord": RandomWords().get_random_word(),
}

# -------------------------------------------
# Hosts the static frontend on the root path.
# This has to be after API routes, since otherwise they're all overwritten by this
app.mount("/", StaticFiles(directory="../static", html=True), name="static")
app.mount("/", StaticFiles(directory="../static", html=True), name="static")
31 changes: 31 additions & 0 deletions dev_run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash

# This script sets up the environment for running the dev server.

# Install frontend dependencies
cd frontend
npm install

# Build frontend and get its PID
npm run build:dev &
pid1=$!
cd ..

# Install backend dependencies
cd backend
pip install -r requirements.txt

# Run backend and get its PID
uvicorn main:app --reload &
pid2=$!

# Define a function to kill both processes
kill_processes() {
kill $pid1 $pid2
}

# Catch SIGINT and SIGTERM signals and kill processes
trap kill_processes SIGINT SIGTERM

# Wait for both processes to finish
wait $pid1 $pid2
2 changes: 1 addition & 1 deletion frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import reactPlugin from "eslint-plugin-react";

export default [
{
files: ["*.tsx", "*.ts", "*.jsx", "*.js", "*.cjs"],
files: ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js", "**/*.cjs"],
ignores: ["node_modules/**/*"],
languageOptions: {
parser: tsParser,
Expand Down
Loading

0 comments on commit abdb229

Please sign in to comment.