Fix more bugs

Co-authored-by: Ezequiel Bellver <ebellver@itba.edu.ar>
Co-authored-by: Juan Barmasch <jbarmasch@itba.edu.ar>
This commit is contained in:
Santiago Lo Coco 2022-12-20 14:46:19 -03:00
parent 2c0c5b49bf
commit daa8418332
20 changed files with 366 additions and 197 deletions

View File

@ -45,11 +45,20 @@ def edit_data(aux: DocumentUpdate, id: str):
mongo.edit_data(id, aux.data)
if aux.access is not None:
mongo.edit_access(id, aux.access)
if aux.public is not None:
print("public")
mongo.make_public(id, aux.public)
if aux.name is not None:
mongo.edit_name(id, aux.name)
return JSONResponse(content={"detail": "Document updated."}, status_code=202)
@router.delete("/{id}")
def delete_doc(id: str):
mongo.delete_document(id)
return JSONResponse(content={"detail": "Document deleted."}, status_code=201)
@router.get("/{id}/access")
def get_doc_access(id: str):
return postgres.access_to_doc(id)
@ -61,20 +70,18 @@ def get_access(id: str, user: tuple = Depends(get_current_user)):
@router.post("/{id}/access")
def give_access(id: str, access_type: Access, user: tuple = Depends(get_current_user)):
postgres.give_access_doc(user[0], id, access_type.access_type)
return JSONResponse(content={"detail": "Access created"}, status_code=201)
@router.put("/{id}/access")
def update_access(
id: str, access_type: Access, user: tuple = Depends(get_current_user)
):
postgres.give_access_doc(user[0], id, access_type.access_type)
def add_access(id: str, aux: Access):
postgres.give_access_doc(aux.username, id, aux.access_type)
return JSONResponse(content={"detail": "Access updated"}, status_code=202)
@router.delete("/{id}/access")
@router.delete("/{id}/access/me")
def deny_access(id: str, user: tuple = Depends(get_current_user)):
postgres.deny_access_doc(user[0], id)
return JSONResponse(content={"detail": "Access deleted"}, status_code=201)
@router.delete("/{id}/access")
def deny_access(id: str, aux: Access):
postgres.deny_access_doc(aux.username, id)
return JSONResponse(content={"detail": "Access deleted"}, status_code=201)

View File

@ -1,8 +1,10 @@
import json
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from bsition.api.models.document import Access
from bsition.api.models.table import Filter, Sort, Table
from bsition.api.models.table import Filter, Sort, Table, Column
from bsition.api.utils.security import get_current_user
from bsition.backend.mongo import tables as mongo
from bsition.backend.postgres import relations as postgres_r
@ -13,53 +15,47 @@ router = APIRouter()
@router.post("")
def create_table(aux: Table):
mongo.create_table(aux.name)
print(json.loads(json.dumps(aux.__dict__)))
mongo.create_table({
"_id": aux.id,
"name": aux.name,
"column_names": aux.column_names,
"types": aux.types,
"owner": aux.owner,
"data": aux.data
})
return JSONResponse(content={"detail": "Table created."}, status_code=201)
@router.put("/{name}")
def edit_table(aux: Table, name: str):
if aux.column is not None and aux.type is not None:
mongo.add_column(name, aux.column, aux.type)
if aux.column_data is not None:
mongo.insert_columns(name, aux.column_data)
if aux.row_number is not None:
mongo.edit_columns(name, aux.columns, aux.columns_data, aux.row_number)
@router.put("/{id}")
def edit_table(aux: Column, id: int):
if aux.data is not None:
mongo.edit_column(aux.row_number, aux.column, aux.data, id)
if aux.column is not None and aux.data is None:
mongo.insert_columns(id, aux.column, aux.type)
return JSONResponse(content={"detail": "Table updated."}, status_code=202)
@router.post("/{name}/sort")
def create_sort(name: str):
postgres_t.create_sort()
return JSONResponse(content={"detail": "Sort created."}, status_code=201)
@router.put("/{name}/sort")
def add_sort(aux: Sort, name: str):
postgres_t.add_sort(name, aux.property, aux.order, aux.priority)
@router.put("/{id}/sort")
def add_sort(aux: Sort, id: str):
postgres_t.add_sort(id, aux.property, aux.order, aux.priority)
return JSONResponse(content={"detail": "Sort updated."}, status_code=202)
@router.get("/{name}/sort")
def sort(name: str):
return mongo.sort(name)
@router.get("/{id}/sort")
def sort(id: str):
return mongo.sort(int(id))
@router.post("/{name}/filter")
def create_filter(name: str):
postgres_t.create_filter()
return JSONResponse(content={"detail": "Filter created."}, status_code=201)
@router.put("/{name}/filter")
def add_filter(aux: Filter, name: str):
postgres_t.add_filter(name, aux.property, aux.value, aux.function)
@router.put("/{id}/filter")
def add_filter(aux: Filter, id: str):
postgres_t.add_filter(id, aux.property, aux.value, aux.function)
return JSONResponse(content={"detail": "Filter updated."}, status_code=202)
@router.get("/{name}/filter")
def filter(name: str):
return mongo.filter(name)
@router.get("/{id}/filter")
def filter(id: str):
return mongo.filter(int(id))
@router.get("/{id}/access")

View File

@ -2,7 +2,6 @@ from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordRequestForm
from bsition.api.models.user import User
from bsition.api.utils.jwt import write_token
from bsition.api.utils.password import verify_password
from bsition.api.utils.security import get_current_user, oauth2_scheme

View File

@ -32,7 +32,9 @@ router.dependencies = [Depends(get_current_user)]
@router.get("")
def get_users():
def get_users(user: str = None):
if user is not None:
return postgres.get_user_by_username(user)
return postgres.get_users()

View File

@ -10,8 +10,10 @@ class Document(BaseModel):
class DocumentUpdate(BaseModel):
name: Optional[str]
access: Optional[list]
public: Optional[bool]
data: Optional[str]
class Access(BaseModel):
access_type: int
username: Optional[str]
access_type: Optional[str]

View File

@ -3,13 +3,20 @@ from pydantic.utils import Optional
class Table(BaseModel):
id: Optional[int]
name: Optional[str]
owner: Optional[str]
column_names: Optional[list]
types: Optional[list]
data: Optional[list]
class Column(BaseModel):
column: Optional[str]
type: Optional[str]
column_data: Optional[list]
row_number: Optional[str]
columns: Optional[list]
columns_data: Optional[list]
row_number: Optional[int]
column_data: Optional[str]
data: Optional[str]
class Sort(BaseModel):

View File

@ -1,12 +1,13 @@
from dotenv import load_dotenv
from bsition.backend.elastic.utils import create_index
from bsition.backend.postgres.tables import add_function
from bsition.backend.postgres.tables import create_filter, create_sort
from bsition.backend.postgres.users import create_user_table
def configure():
load_dotenv()
add_function()
create_user_table()
create_filter()
create_sort()
create_index("test-index")

View File

@ -67,3 +67,15 @@ def edit_name(id, name):
dbname = get_database()
docs_coll = dbname["docs"]
docs_coll.update_one({"_id": ObjectId(id)}, {"$set": {"name": name}})
def make_public(id, public):
dbname = get_database()
docs_coll = dbname["docs"]
docs_coll.update_one({"_id": ObjectId(id)}, {"$set": {"public": public}})
def delete_document(id):
dbname = get_database()
docs_coll = dbname["docs"]
docs_coll.delete_one({"_id": ObjectId(id)})

View File

@ -8,7 +8,7 @@ def create_table(table):
docs_coll.insert_one(table)
def insert_columns(id, name, type, data):
def insert_columns(id, name, type):
dbname = get_database()
docs_coll = dbname["tables"]
doc = docs_coll.find_one({"_id": id}, {"_id": 0})

View File

@ -106,7 +106,7 @@ def access_to_doc(id):
cur = conn.cursor()
cur.execute(
sql.SQL(
"SELECT user_id, username, access_type FROM doc_access JOIN users ON user_id = users.id WHERE doc_id = {id}"
"SELECT user_id, username, access_type FROM doc_access JOIN users ON user_id = users.id WHERE doc_id = {id} ORDER BY user_id"
).format(
id=sql.Literal(id),
)
@ -114,6 +114,19 @@ def access_to_doc(id):
return list(cur.fetchall())
def get_shared_documents(user_id):
conn = get_connection()
cur = conn.cursor()
cur.execute(
sql.SQL(
"SELECT doc_id FROM doc_access WHERE user_id = {user_id} AND access_type"
).format(
id=sql.Literal(user_id),
)
)
return list(cur.fetchall())
def is_public(doc_id):
conn = get_connection()
cur = conn.cursor()

View File

@ -35,52 +35,6 @@ def get_sort(id):
return cur.fetchall()
# def add_function():
# conn = get_connection()
# cur = conn.cursor()
# cur.execute(
# """
# CREATE OR REPLACE FUNCTION trigger_function()
# RETURNS TRIGGER
# LANGUAGE PLPGSQL
# AS $$
# DECLARE
# name text := TG_ARGV[0]::text;
# BEGIN
# IF NEW.property NOT IN (
# SELECT column_name
# FROM INFORMATION_SCHEMA.COLUMNS
# WHERE TABLE_NAME = name)
# THEN
# RAISE EXCEPTION 'ERROR %', NEW.property;
#
# END IF;
#
# RETURN NEW;
# END;
# $$;
# """
# )
# conn.commit()
#
#
# def add_filter_trigger(name):
# conn = get_connection()
# cur = conn.cursor()
# cur.execute(
# sql.SQL(
# """
# CREATE TRIGGER {filter}
# BEFORE INSERT OR UPDATE
# ON {filter}
# FOR EACH ROW
# EXECUTE PROCEDURE trigger_function({table});
# """
# ).format(table=sql.Identifier(name), filter=sql.Identifier(name + "_filter"))
# )
# conn.commit()
def create_filter():
conn = get_connection()
cur = conn.cursor()

View File

@ -45,7 +45,7 @@ def get_user_by_username(username):
conn = get_connection()
cur = conn.cursor()
cur.execute(
sql.SQL("SELECT * FROM users WHERE username = {username}").format(
sql.SQL("SELECT * FROM users WHERE LOWER(username) = LOWER({username})").format(
username=sql.Literal(username)
)
)

View File

@ -8,6 +8,8 @@ import {
LogoIcon,
LogoutIcon,
SearchIcon,
ShareIcon,
ShareDocIcon,
HomeIcon,
CreateIcon
} from "./icons";
@ -16,9 +18,9 @@ const Sidebar = () => {
const [toggleCollapse, setToggleCollapse] = useState(false);
const [isCollapsible, setIsCollapsible] = useState(false);
const [menuItems, setMenuItems] = useState([
{id: 1, label: "Home", icon: HomeIcon, link: "/"},
{id: 2, label: "Search", icon: SearchIcon, link: "/search"},
{id: 3, label: "Create document", icon: CreateIcon, link: "/create-document"}
{id: 1, label: "Home", icon: HomeIcon, shared: false, link: "/"},
{id: 2, label: "Search", icon: SearchIcon, shared: false, link: "/search"},
{id: 3, label: "Create document", icon: CreateIcon, shared: false, link: "/create-document"}
]);
const [token, setToken] = useState("");
@ -37,15 +39,17 @@ const Sidebar = () => {
})
let json = await res.json();
let list = [
{id: 1, label: "Home", icon: HomeIcon, link: "/"},
{id: 2, label: "Search", icon: SearchIcon, link: "/search"},
{id: 3, label: "Create document", icon: CreateIcon, link: "/create-document"}
{id: 1, label: "Home", icon: HomeIcon, shared: false, link: "/"},
{id: 2, label: "Search", icon: SearchIcon, shared: false, link: "/search"},
{id: 3, label: "Create document", icon: CreateIcon, shared: false, link: "/create-document"}
]
json.forEach((doc) => {
let shared = doc["owner"].toString() !== localStorage.getItem("user")
list.push({
id: doc["id"],
label: doc["name"],
icon: ArticleIcon,
icon: shared ? ShareDocIcon : ArticleIcon,
shared: shared,
link: `/documents/${doc["id"]}`
})
})

View File

@ -6,7 +6,7 @@ function PublicIcon({fill = "#6C7281", ...rest}) {
width={24}
height={24}
fill="none"
viewBox="0 0 24 24"
viewBox="23.9516 24.0793 208 207.9"
xmlns="http://www.w3.org/2000/svg"
{...rest}
>

View File

@ -0,0 +1,21 @@
import * as React from "react";
function ShareDocIcon({fill = "#6C7281", ...rest}) {
return (
<svg
width={22}
height={22}
fill="none"
viewBox="2 2 21.8 18"
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
<path
d="M 18 4 v 14 H 4 V 4 h 14 z m 0 -2 H 4 c -1.1 0 -2 0.9 -2 2 v 14 c 0 1.1 0.9 2 2 2 h 14 c 1.1 0 2 -0.9 2 -2 V 4 c 0 -1.1 -0.9 -2 -2 -2 z M 13 16 z m 3 -4 z m 0 -4 H 6 V 6 h 10 v 2 z m 3.3 -0.2 a 2.25 2.25 90 1 1 0.5427 1.4652 l -6.0462 2.808 a 2.2491 2.2491 90 0 1 0 1.3536 l 6.0462 2.808 a 2.25 2.25 90 1 1 -0.4392 0.7884 l -6.0462 -2.808 a 2.25 2.25 90 1 1 0 -2.9304 l 6.0462 -2.808 a 2.25 2.25 90 0 1 -0.1035 -0.6768 z"
fill={fill}
/>
</svg>
);
}
export default ShareDocIcon;

View File

@ -13,4 +13,5 @@ export {default as DeleteIcon} from "./DeleteIcon";
export {default as OptionsIcon} from "./OptionsIcon";
export {default as ManageIcon} from "./ManageIcon";
export {default as CreateIcon} from "./CreateIcon";
export {default as ShareDocIcon} from "./ShareDocIcon";

View File

@ -1,16 +1,8 @@
import io from "socket.io-client";
import {useState, useEffect} from "react";
import React, {useEffect, useState} from "react";
import {useRouter} from "next/router";
import Layout from "../../components/Layout";
import {
OptionsIcon,
DeleteIcon,
LockIcon,
PublicIcon,
ShareIcon,
ManageIcon
} from "../../components/icons";
import Link from "next/link";
import {DeleteIcon, LockIcon, ManageIcon, OptionsIcon, ShareIcon, PublicIcon} from "../../components/icons";
let socket;
@ -20,6 +12,8 @@ export default function Document() {
const [text, setText] = useState("");
const [token, setToken] = useState("");
const [title, setTitle] = useState("");
const [shared, setShared] = useState(false);
const [publicMode, setPublicMode] = useState(false);
useEffect(() => {
socketInitializer();
@ -41,6 +35,8 @@ export default function Document() {
const json = await res.json();
setText(() => json["data"])
setTitle(() => json["name"])
setShared(() => json["owner"].toString() !== localStorage.getItem("user"))
setPublicMode(() => json["public"])
}
fetchData()
@ -52,7 +48,7 @@ export default function Document() {
const interval = setInterval(() => {
fetch(`http://localhost:8000/api/documents/${router.query.id}`, {
method: 'PUT',
body: `{"data": "${text}"}`,
body: `{"data": "${text.replace(/\n/g, "\\n")}"}`,
headers: {
'Content-type': 'application/json',
'Authorization': `Bearer ${token}`
@ -89,6 +85,48 @@ export default function Document() {
};
let access = router.query.id + "/access"
const deleteDoc = async () => {
const res = await fetch(`http://localhost:8000/api/documents/${router.query.id}`, {
method: "DELETE",
headers: {
'Authorization': `Bearer ${token}`
}
})
const json = await res.json();
if (res.status == 201) {
router.push("/")
}
};
const publicOrPrivate = async () => {
if (!publicMode) {
const res = await fetch(`http://localhost:8000/api/documents/${router.query.id}`, {
method: "PUT",
headers: {
'Content-type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: '{"public": true}'
})
if (res.status == 202) {
setPublicMode(() => true)
}
} else {
const res = await fetch(`http://localhost:8000/api/documents/${router.query.id}`, {
method: 'PUT',
headers: {
'Content-type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: '{"public": false}'
})
if (res.status == 202) {
setPublicMode(() => false)
}
}
};
return (
<Layout>
@ -98,6 +136,7 @@ export default function Document() {
{title}
</span>
<div className="dropdown">
{!shared && (<>
<OptionsIcon onClick={(e) => {
document.getElementById("myDropdown").classList.toggle("show");
@ -115,23 +154,27 @@ export default function Document() {
}
}} className="dropbtn"></OptionsIcon>
<div id="myDropdown" className="dropdown-content">
<Link href={access}>
<a href={access}>
<div className="flex flex-row items-center justify-start align-center menu-opt">
<ManageIcon/><span>Manage access</span></div>
</Link>
<a href="#">
<div className="flex flex-row items-center justify-start align-center menu-opt"><ShareIcon/><span>Share</span>
<ManageIcon/><span>Manage access</span>
</div>
</a>
<a href="#">
{publicMode && <a onClick={publicOrPrivate}>
<div className="flex flex-row items-center justify-start align-center menu-opt">
<LockIcon/><span>Public/private</span></div>
<LockIcon/><span>Make private</span></div>
</a>
<a href="#">
}
{!publicMode && <a onClick={publicOrPrivate}>
<div className="flex flex-row items-center justify-start align-center menu-opt">
<PublicIcon/><span>Make public</span></div>
</a>
}
<a onClick={deleteDoc}>
<div className="flex flex-row items-center justify-start align-center menu-opt">
<DeleteIcon/><span>Delete document</span></div>
</a>
</div>
</>)}
</div>
</div>
<textarea

View File

@ -1,22 +1,14 @@
import React, {useState, useEffect} from "react";
import React, {useEffect, useState} from "react";
import {useRouter} from "next/router";
import Layout from "../../../components/Layout";
import {
OptionsIcon,
DeleteIcon,
LockIcon,
PublicIcon,
ShareIcon,
ManageIcon
} from "../../../components/icons";
import Link from "next/link";
import classNames from "classnames";
import {DeleteIcon} from "../../../components/icons";
export default function Document() {
const router = useRouter();
const [token, setToken] = useState("");
const [users, setUsers] = useState([]);
const [update, setUpdate] = useState(false);
useEffect(() => {
setToken(() => localStorage.getItem("token"))
@ -32,7 +24,6 @@ export default function Document() {
}
})
const json = await res.json();
console.log(json)
let list = []
json.forEach((user) => {
list.push({
@ -46,18 +37,24 @@ export default function Document() {
fetchData()
}
}, [token, router.query.id])
}, [token, router.query.id, update])
const handleSubmit = async (event) => {
const handleSubmitAccess = async (event, username) => {
event.preventDefault()
console.log(event.target.value)
console.log(event.target)
let data = {
name: event.target.name.value,
username: username,
access_type: event.target.value,
}
const JSONdata = JSON.stringify(data)
const res = await fetch("http://localhost:8000/api/documents", {
console.log(JSONdata)
const res = await fetch(`http://localhost:8000/api/documents/${router.query.id}/access`, {
method: "POST",
headers: {
'Content-Type': 'application/json',
@ -66,49 +63,125 @@ export default function Document() {
body: JSONdata
})
let json = await res.json();
// setUsers(() => [])
setUpdate(() => update == true ? false : true)
}
const handleSubmitDelete = async (event, username) => {
event.preventDefault()
let data = {
username: username,
}
const JSONdata = JSON.stringify(data)
const res = await fetch(`http://localhost:8000/api/documents/${router.query.id}/access`, {
method: "DELETE",
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSONdata
})
let json = await res.json();
setUsers(() => [])
setUpdate(() => update == true ? false : true)
}
const handleSubmit = async (event) => {
event.preventDefault()
const aux = await fetch(`http://localhost:8000/api/users?` + new URLSearchParams(
{user: event.target.name.value}
), {
method: "GET",
headers: {
'Authorization': `Bearer ${token}`
}
})
const pr = await aux.json();
let user_id = pr[0]
let data = {
username: user_id,
access_type: event.target.select.value,
}
const JSONdata = JSON.stringify(data)
const res = await fetch(`http://localhost:8000/api/documents/${router.query.id}/access`, {
method: "POST",
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSONdata
})
let json = await res.json();
setUpdate(() => update == true ? false : true)
}
return (
<Layout>
<div style={{height: "50px", justifyContent: "space-between"}}
<div style={{justifyContent: "space-between"}}
className="align-center flex">
<table>
<thead>
<tr>
<th>Username</th>
<th>Access</th>
<th>Access level</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
{users.map(({icon: Icon, ...user}) => {
return (
<>
<tr style={{color: "black"}}>
<tr style={{color: "black"}} key={user.id}>
<td>{user.username}</td>
<td>
<select style={{padding: "4px"}} value={user.access} onChange={handleSubmit}>
<select style={{padding: "4px"}} value={user.access}
onChange={(e) => handleSubmitAccess(e, user.id)}>
<option value={1} label="Full-access"></option>
<option value={2} label="Edit"></option>
<option value={3} label="Read-only"></option>
</select>
</td>
<td>
<DeleteIcon onClick={(e) => handleSubmitDelete(e, user.id)}/>
</td>
</tr>
);
})}
</tbody>
</table>
<div className="flex items-center justify-center w-full h-full">
{/*<form onSubmit={handleSubmit} className="flex items-center justify-center align-center w-full h-full login-form" style={{flexDirection: "column"}}>*/}
<form onSubmit={handleSubmit}
className="flex items-center justify-center align-center w-full h-full login-form"
style={{flexDirection: "column"}}>
<div>
<input type="text" id="name" name="name" required placeholder="name"
style={{border: "1px solid grey", color: "black", padding: "2px 4px"}}/>
</div>
<div>
<select id="select" style={{padding: "4px"}}>
<option value={1} label="Full-access"></option>
<option value={2} label="Edit"></option>
<option value={3} label="Read-only"></option>
</select>
</div>
<button type="submit" className="rounded bg-pink-600 rounded"
style={{border: "1px solid grey", padding: "4px"}}>Create
style={{border: "1px solid grey", padding: "4px"}}>Add user
</button>
{/*</form>*/}
</form>
</div>
</>
);
})}
</table>
{/*</>*/}
{/* );*/}
{/*})}*/}
</div>
</Layout>
);

View File

@ -105,3 +105,32 @@ li {
table {
all: revert
}
table {
width: 800px;
border-collapse: collapse;
overflow: hidden;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
}
th,
td {
padding: 15px;
background-color: rgba(255,255,255,0.2);
color: #fff;
}
th {
text-align: left;
}
thead th {
background-color: #55608f;
}
tbody tr:hover {
background-color: rgba(255,255,255,0.3);
}
tbody td {
position: relative;
}
select {
color: black
}

7
run.sh
View File

@ -9,18 +9,20 @@ usage: ${0##*/} [command]
-d Run docker-compose up.
-c Configure databases.
-f Build and run frontend.
-t Run frontend in dev-mode.
EOF
exit 1
}
RUN=
while getopts "hadicf" OPTION; do
while getopts "hadicft" OPTION; do
case $OPTION in
a) RUN=api ;;
d) RUN=docker ;;
i) RUN=install ;;
c) RUN=configure ;;
f) RUN=front ;;
t) RUN=test;;
*) usage ;;
esac
done
@ -36,6 +38,9 @@ elif [ "$RUN" = 'front' ]; then
npm install
npm run build
npm run start
elif [ "$RUN" = 'test' ]; then
cd bsition/frontend
npm run dev
else
[ ! -d data ] && mkdir data
[ ! -d data/postgres ] && mkdir data/postgres