diff --git a/API/Functions.py b/API/Functions.py index 4858e26a571deb8d84f81efbeb0aeaebae4732ca..88ca32cdf02a8461b6e8f7cc76d7d5e28ab059f1 100644 --- a/API/Functions.py +++ b/API/Functions.py @@ -5,6 +5,7 @@ from dotenv import load_dotenv def safe_url(query): + """ convert query to URL-safe style """ load_dotenv() host = os.getenv('SERVER_HOST') return host + Parser.url_safe(query) @@ -24,6 +25,7 @@ def get(query): def put(query, json_file): + """ PUT request """ print("PUT: " + query) url = safe_url(query) req = requests.put(url, json=json_file) @@ -32,6 +34,7 @@ def put(query, json_file): def post(query, json_file): + """ POST request """ print("POST: " + query) url = safe_url(query) req = requests.post(url, json=json_file) @@ -40,6 +43,7 @@ def post(query, json_file): def delete(query): + """ DELETE request """ print("DELETE: " + query) url = safe_url(query) req = requests.delete(url) diff --git a/Crawler/Scrape.py b/Crawler/Scrape.py index a2f97aefca3dde45cec48686887942fcfbc52771..bb0c4a9a300a56ed1cf6d781071880358016993c 100644 --- a/Crawler/Scrape.py +++ b/Crawler/Scrape.py @@ -4,6 +4,7 @@ from DataBase import mongoDB as db def scrape_api(url_api, book_num_api, author_num_api): + """ Function called by other module to run the scraper """ print('Request received, start scraping') book_dict_api = {} author_dict_api = {} diff --git a/DataBase/JsonParser.py b/DataBase/JsonParser.py index 2a5e5247d8663b0d08c0a921c5b79317307d0e26..fa805723b49854ee50c49e375bcbdc2daa4a0c5d 100644 --- a/DataBase/JsonParser.py +++ b/DataBase/JsonParser.py @@ -1,5 +1,6 @@ def parse_book_dict_to_json(dictionary): + """ parse books dictionary to json file """ item_list = list(dictionary.items()) return_list = [] for items in item_list: diff --git a/DataBase/mongoDB.py b/DataBase/mongoDB.py index ef437fce2c84554924a3b4a118cacd9010c77783..1072256e8e01d6fed2f7194bcafa4c1ce3901ebf 100644 --- a/DataBase/mongoDB.py +++ b/DataBase/mongoDB.py @@ -16,9 +16,9 @@ def get_db(): def insert_document(docu, opt): db = get_db() if opt == 0: - records = db.test_books + records = db.books elif opt == 1: - records = db.test_authors + records = db.authors else: print("failed to get json file: wrong opt for selecting collection") return @@ -34,9 +34,9 @@ def insert_dicts(dictionary, opt): """ db = get_db() if opt == 0: - records = db.test_books + records = db.books elif opt == 1: - records = db.test_authors + records = db.authors else: print("failed to get json file: wrong opt for selecting collection") return @@ -58,12 +58,13 @@ def update_dicts(opt, identifier, content): """ db = get_db() if opt == 0: - records = db.test_books + records = db.books elif opt == 1: - records = db.test_authors + records = db.authors else: print("failed to get json file: wrong opt for selecting collection") return + print(content) result = records.update_one( identifier, {"$set": content}, @@ -82,9 +83,9 @@ def get_documents_json(opt, identifier): """ db = get_db() if opt == 0: - records = db.test_books + records = db.books elif opt == 1: - records = db.test_authors + records = db.authors else: print("failed to get json file: wrong opt for selecting collection") return json.dumps({}) @@ -127,10 +128,10 @@ def clean(opt, identifier): """ db = get_db() if opt == 0: - records = db.test_books + records = db.books elif opt == 1: - records = db.test_authors + records = db.authors else: print("failed to get json file: wrong opt for selecting collection") - return - records.delete_many(identifier) + return 0 + return records.delete_many(identifier).deleted_count diff --git a/ManualTest.md b/ManualTest.md new file mode 100644 index 0000000000000000000000000000000000000000..5367e98eadd3dc95964d5575abcfc730de42efea --- /dev/null +++ b/ManualTest.md @@ -0,0 +1,194 @@ +# GoodRead Crawler Manual Test + +This is a manual test plan for cs242-sp21 assignment2: GoodRead Crawler + +## System Requirements + + +#### OS: +The following OS are appropriate system for running the GoodRead Crawler application and testing. +- Windows 7 or above +- Mac OS X +- Linux + +#### Testing Prerequisites +Softwares/applications are required for running the test. + +- Python 3 +- Javascript +- React JS + + +## Scenes for Testing +.png) + +## 1.GET function + +#### Test1.1 +- test content: + Test GET exist book data +- expect result: + Successfully get book data and showing in table +.png) + +#### Test1.2 +- test content: + Test GET exist author data +- expect result: + Successfully get author data and showing in table +.png) + +#### Test1.3 +- test content: + Test GET unexist book data +- expect result: + Request failed +.png) + +#### Test1.4 +- test content: + Test GET unexist author data +- expect result: + Request failed +.png) + +#### Test1.5 +- test content: + Test search exist book data with id +- expect result: + Successfully find the data and print in table +.png) + +#### Test1.6 +- test content: + Test search exist book data with flexible search condition +- expect result: + Successfully find the data and showing in table +.png) + +#### Test1.7 +- test content: + Test search unexist book data +- expect result: + Request failed +.png) + +#### Test1.8 +- test content: + Test using scrape in GET command +- expect result: + Request failed +.png) + +## 2.PUT function + +#### Test2.1 +- test content: + Test PUT update data for exist book documentation +- expect result: + No new document is created, existing document updated +.png) +.png) +.png) + +#### Test2.2 +- test content: + Test PUT new book documentation +- expect result: + A new book document is created +.png) +.png) + +#### Test2.3 +- test content: + Test PUT update data for exist author documentation +- expect result: + No new document is created, existing document updated +.png) +.png) +.png) + +#### Test2.4 +- test content: + Test PUT new author documentation +- expect result: + A new author document is created +.png) +.png) + + +## 3.POST function + +#### Test3.1 +- test content: + Test POST book data +- expect result: + Successfully post new book documtent +.png) +.png) + + +#### Test3.2 +- test content: + Test POST author data +- expect result: + Successfully post new author documtent +.png) +.png) + +#### Test3.3 +- test content: + Test Scrape data +- expect result: + New book and author data is added into collections +.png) +.png) + +#### Test3.4 +- test content: + Test the input form of book-type doc can be closed by click 'back' button and another form of author-type can be opened +- expect result: + Successfully changed the form +.png) +.png) + +## 4.DELETE function + +#### Test4.1 +- test content: + Test DELETE book data by id +- expect result: + Successfully delete the book data +.png) + +#### Test4.2 +- test content: + Test DELETE author data by name +- expect result: + Successfully delete the author data +.png) + +#### Test4.3 +- test content: + Test DELETE unexist data +- expect result: + Request failed +.png) + +## 5.Responsive UI Test + +#### Test5.1 +- test content: + Test main page can fit mobile device +- expect result: + All elements can be fitted in screen of mobile-device size +.png) + +#### Test5.2 +- test content: + Test data table can scroll +- expect result: + Data table can be scroll to right and left when the screen's size is not large enough +.png) +.png) +.png) diff --git a/ManualTestPictures/1(1).png b/ManualTestPictures/1(1).png new file mode 100644 index 0000000000000000000000000000000000000000..bb437397c142ee4232d32d5026c6f3cbf5888883 Binary files /dev/null and b/ManualTestPictures/1(1).png differ diff --git a/ManualTestPictures/1(10).png b/ManualTestPictures/1(10).png new file mode 100644 index 0000000000000000000000000000000000000000..7f601e45013cb14660c7e375a3ac3eb9142bd6d8 Binary files /dev/null and b/ManualTestPictures/1(10).png differ diff --git a/ManualTestPictures/1(11).png b/ManualTestPictures/1(11).png new file mode 100644 index 0000000000000000000000000000000000000000..4c6a9f0907ced79d183ad8ebd9ce812cf3e52eb7 Binary files /dev/null and b/ManualTestPictures/1(11).png differ diff --git a/ManualTestPictures/1(12).png b/ManualTestPictures/1(12).png new file mode 100644 index 0000000000000000000000000000000000000000..893c8d7272f037899890293826e60a41db7dde28 Binary files /dev/null and b/ManualTestPictures/1(12).png differ diff --git a/ManualTestPictures/1(13).png b/ManualTestPictures/1(13).png new file mode 100644 index 0000000000000000000000000000000000000000..19d06b7b94e853ce18d13dad7e45fb10cd5ebdd8 Binary files /dev/null and b/ManualTestPictures/1(13).png differ diff --git a/ManualTestPictures/1(14).png b/ManualTestPictures/1(14).png new file mode 100644 index 0000000000000000000000000000000000000000..097f76faa00adf8d52dbaf2705a56d96858a9a02 Binary files /dev/null and b/ManualTestPictures/1(14).png differ diff --git a/ManualTestPictures/1(15).png b/ManualTestPictures/1(15).png new file mode 100644 index 0000000000000000000000000000000000000000..d72f119d0e141018f678922e1d8ede3063380706 Binary files /dev/null and b/ManualTestPictures/1(15).png differ diff --git a/ManualTestPictures/1(16).png b/ManualTestPictures/1(16).png new file mode 100644 index 0000000000000000000000000000000000000000..574b028828c023fa85d82525fa1f4dba6b95c41a Binary files /dev/null and b/ManualTestPictures/1(16).png differ diff --git a/ManualTestPictures/1(17).png b/ManualTestPictures/1(17).png new file mode 100644 index 0000000000000000000000000000000000000000..a2abcd90a2b9e83d3210cf3ceb8c24cbdd429b45 Binary files /dev/null and b/ManualTestPictures/1(17).png differ diff --git a/ManualTestPictures/1(18).png b/ManualTestPictures/1(18).png new file mode 100644 index 0000000000000000000000000000000000000000..eb5050e53bb5d9955d7691435fbfbbfe91da5d8d Binary files /dev/null and b/ManualTestPictures/1(18).png differ diff --git a/ManualTestPictures/1(19).png b/ManualTestPictures/1(19).png new file mode 100644 index 0000000000000000000000000000000000000000..e7292195a4b80ab5976fea51ee03aaeb41b652de Binary files /dev/null and b/ManualTestPictures/1(19).png differ diff --git a/ManualTestPictures/1(2).png b/ManualTestPictures/1(2).png new file mode 100644 index 0000000000000000000000000000000000000000..9e5fb84266b77a39f1c7f2e4f0661645ed37afda Binary files /dev/null and b/ManualTestPictures/1(2).png differ diff --git a/ManualTestPictures/1(20).png b/ManualTestPictures/1(20).png new file mode 100644 index 0000000000000000000000000000000000000000..419607853c30236feaae1967fa88b1603637f3db Binary files /dev/null and b/ManualTestPictures/1(20).png differ diff --git a/ManualTestPictures/1(21).png b/ManualTestPictures/1(21).png new file mode 100644 index 0000000000000000000000000000000000000000..41ee7ea4168d90b873daccd1244317f9bbed33ef Binary files /dev/null and b/ManualTestPictures/1(21).png differ diff --git a/ManualTestPictures/1(22).png b/ManualTestPictures/1(22).png new file mode 100644 index 0000000000000000000000000000000000000000..c6f6c4a09669d06a0f806fd4e9523f9ca027a8d5 Binary files /dev/null and b/ManualTestPictures/1(22).png differ diff --git a/ManualTestPictures/1(23).png b/ManualTestPictures/1(23).png new file mode 100644 index 0000000000000000000000000000000000000000..488a1dbca0b5be3eea3f4ccb176f4ed15a4fbd39 Binary files /dev/null and b/ManualTestPictures/1(23).png differ diff --git a/ManualTestPictures/1(24).png b/ManualTestPictures/1(24).png new file mode 100644 index 0000000000000000000000000000000000000000..f3637fb440cbef3b0d24d4e12607e99c1ca4a46a Binary files /dev/null and b/ManualTestPictures/1(24).png differ diff --git a/ManualTestPictures/1(25).png b/ManualTestPictures/1(25).png new file mode 100644 index 0000000000000000000000000000000000000000..3ce602746194e20f7b4a55e07a945067c0ab3dab Binary files /dev/null and b/ManualTestPictures/1(25).png differ diff --git a/ManualTestPictures/1(26).png b/ManualTestPictures/1(26).png new file mode 100644 index 0000000000000000000000000000000000000000..49d7ae60bc1eed4b8a3da127ea6cdbf70d8cd884 Binary files /dev/null and b/ManualTestPictures/1(26).png differ diff --git a/ManualTestPictures/1(27).png b/ManualTestPictures/1(27).png new file mode 100644 index 0000000000000000000000000000000000000000..ce54d5fdc51b64b6ff921d09364282446f562ed3 Binary files /dev/null and b/ManualTestPictures/1(27).png differ diff --git a/ManualTestPictures/1(28).png b/ManualTestPictures/1(28).png new file mode 100644 index 0000000000000000000000000000000000000000..b73b9f1dee6ce1558db4ac4dbf2532a6929abddf Binary files /dev/null and b/ManualTestPictures/1(28).png differ diff --git a/ManualTestPictures/1(29).png b/ManualTestPictures/1(29).png new file mode 100644 index 0000000000000000000000000000000000000000..7aef2893769482716c76b20a6cbb5e930928c8ad Binary files /dev/null and b/ManualTestPictures/1(29).png differ diff --git a/ManualTestPictures/1(3).png b/ManualTestPictures/1(3).png new file mode 100644 index 0000000000000000000000000000000000000000..e3b768c83fae0ec686e7cd12a9a56fa6652967b6 Binary files /dev/null and b/ManualTestPictures/1(3).png differ diff --git a/ManualTestPictures/1(30).png b/ManualTestPictures/1(30).png new file mode 100644 index 0000000000000000000000000000000000000000..5c382a26d3d031a1379196ef63a7292822b55f7c Binary files /dev/null and b/ManualTestPictures/1(30).png differ diff --git a/ManualTestPictures/1(31).png b/ManualTestPictures/1(31).png new file mode 100644 index 0000000000000000000000000000000000000000..eea95251b868948ccddd0a3ae25fabbd56f0d47c Binary files /dev/null and b/ManualTestPictures/1(31).png differ diff --git a/ManualTestPictures/1(32).png b/ManualTestPictures/1(32).png new file mode 100644 index 0000000000000000000000000000000000000000..e66d2d2573b6498276cf0727c9cb28891c7e1c40 Binary files /dev/null and b/ManualTestPictures/1(32).png differ diff --git a/ManualTestPictures/1(33).png b/ManualTestPictures/1(33).png new file mode 100644 index 0000000000000000000000000000000000000000..7e90af77068ae10d0e15d360387d0dec4ef6ef72 Binary files /dev/null and b/ManualTestPictures/1(33).png differ diff --git a/ManualTestPictures/1(34).png b/ManualTestPictures/1(34).png new file mode 100644 index 0000000000000000000000000000000000000000..060c442c3c9255f4d255444c54c69131d3df59e3 Binary files /dev/null and b/ManualTestPictures/1(34).png differ diff --git a/ManualTestPictures/1(35).png b/ManualTestPictures/1(35).png new file mode 100644 index 0000000000000000000000000000000000000000..630e4d96bdb97833e638e4b3ae9df2f3d41cce32 Binary files /dev/null and b/ManualTestPictures/1(35).png differ diff --git a/ManualTestPictures/1(36).png b/ManualTestPictures/1(36).png new file mode 100644 index 0000000000000000000000000000000000000000..ac972262e918c5b0263d3d2dd21e452db18ec0e1 Binary files /dev/null and b/ManualTestPictures/1(36).png differ diff --git a/ManualTestPictures/1(37).png b/ManualTestPictures/1(37).png new file mode 100644 index 0000000000000000000000000000000000000000..8364184f3e5a6db598602f6e5fd8bd8c4e98845b Binary files /dev/null and b/ManualTestPictures/1(37).png differ diff --git a/ManualTestPictures/1(38).png b/ManualTestPictures/1(38).png new file mode 100644 index 0000000000000000000000000000000000000000..494bf6af8f566350b24729a5998f4f8c20a5c8ff Binary files /dev/null and b/ManualTestPictures/1(38).png differ diff --git a/ManualTestPictures/1(39).png b/ManualTestPictures/1(39).png new file mode 100644 index 0000000000000000000000000000000000000000..67ec08f55f3fac0517964f1e9f89581d1567eda6 Binary files /dev/null and b/ManualTestPictures/1(39).png differ diff --git a/ManualTestPictures/1(4).png b/ManualTestPictures/1(4).png new file mode 100644 index 0000000000000000000000000000000000000000..c8ec3d1fe121f7618030d2fab73cc7dd7be92a52 Binary files /dev/null and b/ManualTestPictures/1(4).png differ diff --git a/ManualTestPictures/1(40).png b/ManualTestPictures/1(40).png new file mode 100644 index 0000000000000000000000000000000000000000..af02e1706318ac567fcaa791ff2444e56d4981aa Binary files /dev/null and b/ManualTestPictures/1(40).png differ diff --git a/ManualTestPictures/1(41).png b/ManualTestPictures/1(41).png new file mode 100644 index 0000000000000000000000000000000000000000..0a94119d042088916124856d16130246f99d9353 Binary files /dev/null and b/ManualTestPictures/1(41).png differ diff --git a/ManualTestPictures/1(42).png b/ManualTestPictures/1(42).png new file mode 100644 index 0000000000000000000000000000000000000000..422f04cc1b1912d2c3b3351f1d0bacd880d7224a Binary files /dev/null and b/ManualTestPictures/1(42).png differ diff --git a/ManualTestPictures/1(43).png b/ManualTestPictures/1(43).png new file mode 100644 index 0000000000000000000000000000000000000000..5e4cc792f4cbbdba448482f753f4f5c6fbdde3f9 Binary files /dev/null and b/ManualTestPictures/1(43).png differ diff --git a/ManualTestPictures/1(5).png b/ManualTestPictures/1(5).png new file mode 100644 index 0000000000000000000000000000000000000000..6eee82818c0b0e66bba706f0753c7448406db87f Binary files /dev/null and b/ManualTestPictures/1(5).png differ diff --git a/ManualTestPictures/1(6).png b/ManualTestPictures/1(6).png new file mode 100644 index 0000000000000000000000000000000000000000..da05b568cbc3e62e77ad16751efdb8350e05cc5d Binary files /dev/null and b/ManualTestPictures/1(6).png differ diff --git a/ManualTestPictures/1(7).png b/ManualTestPictures/1(7).png new file mode 100644 index 0000000000000000000000000000000000000000..39a8c55f2ea4ad98bfa080610253e1d037a89a84 Binary files /dev/null and b/ManualTestPictures/1(7).png differ diff --git a/ManualTestPictures/1(8).png b/ManualTestPictures/1(8).png new file mode 100644 index 0000000000000000000000000000000000000000..4e2021cc24ce9a54751302cb26c2e6edbcf67fe5 Binary files /dev/null and b/ManualTestPictures/1(8).png differ diff --git a/ManualTestPictures/1(9).png b/ManualTestPictures/1(9).png new file mode 100644 index 0000000000000000000000000000000000000000..f61bc9ce59d139f225148865f7a8d4624a159e27 Binary files /dev/null and b/ManualTestPictures/1(9).png differ diff --git a/README.md b/README.md index 52026e13ba87f3145b72067e2920db8322249ed8..127b886d46c6f0ab22ba73a4f5d81988a8ded974 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,13 @@ This project is for sp21 CS242 assignment2 Current version is 2.1 -###Function +### Function 1. Scrap data of books and authors from GoodReads 2. Store scraped data on cloud or local files 3. Query cloud for certain documents of books or authors -###Composition: -####Client: Command line interface (interactive) -####Server: Simple server based on Flask -####Database: MongoDB \ No newline at end of file +### Composition: +#### Client: Command line interface (interactive) +#### Server: Simple server based on Flask +#### Database: MongoDB +#### Website: Javascipt HTML with React diff --git a/RegularExpressionParser/Parser.py b/RegularExpressionParser/Parser.py index e361d2ff6b2141a2fa20dc133d29fc1bb631ec46..abb8e0299e6d4df202b168f895786afb818146d0 100644 --- a/RegularExpressionParser/Parser.py +++ b/RegularExpressionParser/Parser.py @@ -20,7 +20,7 @@ def parse_query_to_url(query): if count == 2: # can only be A.B:C or wrong if re.search("^[0-9a-zA-Z_.]+:[0-9a-zA-Z_\".]", query): - return url_safe(elements[0] + "%3A" + elements[1]) + return url_safe(elements[0] + ":3A" + elements[1]) else: print("Invalid query1.") return "" @@ -48,6 +48,12 @@ def parse_query_to_url(query): return "" +def parse_url_to_query(url): + query_str = url.replace("%20", " ").replace("%22", "\"").replace("%3C", "<").replace("%3E", ">").replace("%26", "&") + return query_str.replace(" AND ", "&AND&").replace(" OR ", "&OR&").replace(": ", ":").replace(" > ", ">")\ + .replace(" < ", "<") + + def parse_query_to_json(pair): elements = re.findall("[0-9A-Za-z\"_.]+", pair) count = len(elements) @@ -61,7 +67,7 @@ def parse_query_to_json(pair): else: return {elements[0].split(".")[1]: {"$not": {"$regex": elements[2], "$options": "i"}}} else: - # can be A.B: C or A.B: "C" + # can be A.B: C, A.B: "C", A.B > C, or A.B < C if re.search(":", pair): if re.search("^[0-9.]*$", elements[1]) and not re.search("id", elements[0]): return {elements[0].split(".")[1]: float(elements[1])} @@ -71,6 +77,8 @@ def parse_query_to_json(pair): else: return {elements[0].split(".")[1]: {"$regex": elements[1], "$options": "i"}} else: + if len(elements[0].split(".")) != 2: + return {"wrong": "True"} if re.search(">", pair): return {elements[0].split(".")[1]: {"$gt": float(elements[1])}} elif re.search("<", pair): diff --git a/Server/SimpleServer.py b/Server/SimpleServer.py index 6507c3295f45f588090b8c0e48f0d714687611a0..58b59dd47753aba28f7ee03d0306ab0fa5aa99bf 100644 --- a/Server/SimpleServer.py +++ b/Server/SimpleServer.py @@ -2,99 +2,144 @@ import DataBase.mongoDB as DataBase import RegularExpressionParser.Parser as Parser import re import Crawler.Scrape as Scrape +import json from flask import Flask from flask import request from flask import abort from flask import jsonify from urllib.parse import urlparse, parse_qs +from flask_cors import CORS, cross_origin app = Flask(__name__) -# app.config["DEBUG"] = True +cors = CORS(app) +# cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) - -@app.route("/", methods=['GET']) -def home(): - """ homepage of server """ - return "200: successfully connected to home page\n" +@app.route("/api/book/", methods=["GET", "PUT", "POST", "DELETE", "OPTIONS"]) +def data_base_book(): + print(request.url) + """ data base page of server """ + if request.method == "GET": + url_parsed = urlparse(request.url) + qs_parsed = parse_qs(url_parsed.query) + if qs_parsed == {}: + return DataBase.get_documents_json(0, {}) + result = search_document(["book.id:" + qs_parsed["id"][0]]) + if result == {"wrong": "True"}: + abort(400, "Bad Request") + elif result == json.dumps({'books': []}): + abort(400, "Book not found") + else: + return result + elif request.method == "PUT": + if request.headers["Content-Type"] != "application/json": + abort(415, "content should be JSON file") + print(request) + json_update_info = request.json + opt = 0 + DataBase.update_dicts(opt, request.args.to_dict(), json_update_info) + return "200: PUT succeeded" + elif request.method == "POST": + if request.headers["Content-Type"] != "application/json": + abort(415, "content should be JSON file") + print(request) + json_file = request.json + DataBase.insert_document(json_file, 0) + return "200: POST succeeded" + elif request.method == "DELETE": + identifier = request.args.to_dict() + print(identifier) + opt=0 + deleted_count = DataBase.clean(opt, identifier) + if deleted_count > 0: + return "200: DELETE succeeded" + else: + abort(400, "Failed to delete: target not found") + elif request.method == "OPTIONS": + return '200 OK' -@app.route("/api/<collection>/", methods=["GET", "PUT", "POST", "DELETE"]) -def data_base(collection): - """ data base page of server """ - print("\n===============================\n") - print(collection) - print("\n===============================\n") +@app.route("/api/author/", methods=["GET", "PUT", "POST", "DELETE", "OPTIONS"]) +def data_base_author(): + print(request.url) + """ data base page of server """ if request.method == "GET": - if collection == "book": - url_parsed = urlparse(request.url) - qs_parsed = parse_qs(url_parsed.query) - if qs_parsed == {}: - return jsonify(DataBase.get_documents_json(0, {})) - return jsonify(search_document(["book.id:" + qs_parsed["id"][0]])) - elif collection == "author": - url_parsed = urlparse(request.url) - qs_parsed = parse_qs(url_parsed.query) - if qs_parsed == {}: - return jsonify(DataBase.get_documents_json(1, {})) - return jsonify(search_document(["author.id:" + qs_parsed["id"][0]])) - elif collection == "search": - url_parsed = urlparse(request.url) - qs_parsed = parse_qs(url_parsed.query) - result = jsonify(search_document(qs_parsed["q"][0].split("&"))) - return jsonify(result) + url_parsed = urlparse(request.url) + qs_parsed = parse_qs(url_parsed.query) + if qs_parsed == {}: + return DataBase.get_documents_json(1, {}) + result = search_document(["author.id:" + qs_parsed["id"][0]]) + if result == {"wrong": "True"}: + abort(400, "Bad Request") + elif result == json.dumps({'authors': []}): + abort(400, "Author not found") else: - abort(404) + return result elif request.method == "PUT": if request.headers["Content-Type"] != "application/json": - abort(415) + abort(415, "content should be JSON file") json_update_info = request.json - if collection == "book": - opt = 0 - elif collection == "author": - opt = 1 - else: - abort(404) + opt = 1 DataBase.update_dicts(opt, request.args.to_dict(), json_update_info) return "200: PUT succeeded" elif request.method == "POST": if request.headers["Content-Type"] != "application/json": abort(415, "content should be JSON file") json_file = request.json - if collection == "books": - DataBase.insert_dicts(json_file, 0) - elif collection == "authors": - DataBase.insert_dicts(json_file, 1) - elif collection == "book": - DataBase.insert_document(json_file, 0) - elif collection == "author": - DataBase.insert_document(json_file, 1) - elif collection == "scrape": - param = request.args.to_dict() - url = param["url"] - max_book = param["max_book"] - max_author = param["max_author"] - Scrape.scrape_api(url, max_book, max_author) - return "200: new data has been added to database" - else: - abort(404) + DataBase.insert_document(json_file, 1) return "200: POST succeeded" elif request.method == "DELETE": identifier = request.args.to_dict() print(identifier) - if collection == "book": - opt = 0 - elif collection == "author": - opt = 1 + opt=1 + deleted_count = DataBase.clean(opt, identifier) + if deleted_count > 0: + return "200: DELETE succeeded" else: - abort(404, "Unknown Collection to DELETE") - DataBase.clean(opt, identifier) - return "200: DELETE succeeded" + abort(400, "Failed to delete: target not found") + elif request.method == "OPTIONS": + return '200 OK' + +@app.route("/api/scrape/", methods=["GET", "PUT", "POST", "DELETE", "OPTIONS"]) +def data_base_scrape(): + print(request.url) + param = request.args.to_dict() + url = param["url"] + max_book = param["max_book"] + max_author = param["max_author"] + Scrape.scrape_api(url, max_book, max_author) + return "200: new data has been added to database" + + +@app.route("/api/search/", methods=["GET", "PUT", "POST", "DELETE", "OPTIONS"]) +def data_base_search(): + print(request.url) + url_parsed = urlparse(request.url) + query = Parser.parse_url_to_query(url_parsed.query) + qs_parsed = query.replace("q=", "") + result = search_document(qs_parsed.split("&")) + if result == {"wrong": "True"}: + abort(400, "Bad Request") + elif result == json.dumps({'books': []}): + abort(400, "Target not found") + else: + return result + +# @app.route("/api/", methods=["GET", "PUT", "POST", "DELETE", "OPTIONS"]) +# def data_api(): +# print('====================') +# print(request.url) +# print(request.collection) +# print('====================') + +# return '200: nice!' def search_document(identifiers): """ function used to find one or several document in database """ if len(identifiers) == 1: json_idt = Parser.parse_query_to_json(identifiers[0]) + if json_idt == {"wrong": "True"}: + return {"wrong": "True"} print(json_idt) if re.search("^book.*", identifiers[0]): return DataBase.get_documents_json(0, json_idt) @@ -115,6 +160,8 @@ def search_document(identifiers): opt = 1 json_idt1 = Parser.parse_query_to_json(identifiers[0]) json_idt2 = Parser.parse_query_to_json(identifiers[2]) + if json_idt1 == {"wrong": "True"} or json_idt2 == {"wrong": "True"}: + return {"wrong": "True"} if identifiers[1] == "AND": exp = {"$and": [json_idt1, json_idt2]} elif identifiers[1] == "OR": @@ -126,7 +173,10 @@ def search_document(identifiers): print(exp) return DataBase.get_documents_json(opt, exp) else: - return "Error, unknown identifiers" + print("Error, unknown identifiers") + return {} + + if __name__ == "__main__": diff --git a/Tests/ParserTests.py b/Tests/ParserTests.py index a46915da4b4b7f4fd3fecb9b2544f6fe719bea2a..d1a5bb38f8799e7dd8c937be37aca75ea0d1ad6f 100644 --- a/Tests/ParserTests.py +++ b/Tests/ParserTests.py @@ -34,3 +34,7 @@ class DataBaseTests(unittest.TestCase): output_json = Parser.parse_query_to_json(query) self.assertEqual(output_json, expect_json) + +if __name__ == '__main__': + unittest.main() + diff --git a/Tests/ServerTests.py b/Tests/ServerTests.py index 2bad5db37bf035d513efd4bd12e115425e2c8dfd..e0aff9964a5d8311bb3fb839a8da837c2c4aa6d7 100644 --- a/Tests/ServerTests.py +++ b/Tests/ServerTests.py @@ -44,7 +44,7 @@ class DataBaseTests(unittest.TestCase): db.insert_document(self.test_data1, 0) url = "http://127.0.0.1:5000/api/bookssss?id=38746485" res = requests.get(url) - self.assertEqual(res.status_code, 200) + self.assertEqual(res.status_code, 400) def test_valid_put(self): db.insert_document(self.test_data1, 0) diff --git a/web/app/src/App.css b/web/app/src/App.css new file mode 100644 index 0000000000000000000000000000000000000000..74b5e053450a48a6bdb4d71aad648e7af821975c --- /dev/null +++ b/web/app/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/web/app/src/App.js b/web/app/src/App.js new file mode 100644 index 0000000000000000000000000000000000000000..4fcf33981540bf93c2f72c8d19888402f7770e55 --- /dev/null +++ b/web/app/src/App.js @@ -0,0 +1,383 @@ +import React from 'react'; +import FourButtons from './Components/FourButtons'; +import { Dialog } from 'react-overlay-pack'; +import CorrectSign from './material/sign_correct.png'; +import ErrorSign from './material/sign_error.png'; +import ElementDisplay from './Components/ElementDisplay'; + + +function App() { + /* Activate Hooks */ + const { useState } = React; + + /* Initializing Hooks for component values showed in main page */ + const [queryStr, setQueryStr] = useState('') + const [sign, setSign] = useState(CorrectSign) + const [text, setText] = useState('This area shows the result of requests..') + const [formState, setFormState] = useState('hide') + const [dialogState, setDialogState] = useState(false) + const [renderState, setRenderState] = useState(false) + + /* initializing form-like input of bookData for PUT and POST */ + const [bookURLState, setBookURLState] = useState('') + const [bookTitleState, setBookTitleState] = useState('') + const [bookIDState, setBookIDState] = useState('') + const [bookISBNState, setBookISBNState] = useState('') + const [bookAuthorState, setBookAuthorState] = useState('') + const [bookAuthorURLState, setBookAuthorURLState] = useState('') + const [bookRatingState, setBookRatingState] = useState('') + const [bookRatingCountState, setBookRatingCountState] = useState('') + const [bookReviewCountState, setBookReviewCountState] = useState('') + const [bookImageURLState, setBookImageURLState] = useState('') + const [bookSimilarBooksState, setBookSimilarBooksState] = useState('') + + /* Initializing form-like input of authorData for PUT AND POST */ + const [authorNameState, setAuthorNameState] = useState('') + const [authorURLState, setAuthorURLState] = useState('') + const [authorIDState, setAuthorIDState] = useState('') + const [authorRatingState, setAuthorRatingState] = useState('') + const [authorRatingCountState, setAuthorRatingCountState] = useState('') + const [authorReviewCountState, setAuthorReviewCountState] = useState('') + const [authorImageURLState, setAuthorImageURLState] = useState('') + const [authorRelatedAuthorsState, setAuthorRelatedAuthorsState] = useState('') + const [authorAuthorBooksState, setAuthorAuthorBooksState] = useState('') + + /* Hooks used to show whether the data passed in is bookData or authorData */ + const [dataState, setDataState] = useState({}) + + /* Activating axios */ + const axios = require('axios').default; + var center = {display: 'flex', justifyContent: 'center', alignItems: 'center'} + + /** Function used in GET Requests */ + function get() { + axios.get('http://127.0.0.1:5000/api/' + queryStr) + .then(function (response) { + // handle success + setSign(CorrectSign) + showDialog() + document.getElementById('dialog').value = response.status + ":\n" + response.statusText + if (queryStr.includes('book')) { + setDataState(response.data['books'][0]) + } else { + setDataState(response.data['authors'][0]) + } + setRenderState(true) + }) + .catch(function (error) { + // handle error + setSign(ErrorSign) + showDialog() + document.getElementById('dialog').value = error //.response.status + ":\n" + error.response.statusText + }) + } + + /** Function used in PUT and POST: extract user input of bookData from the form and parse into JSON */ + function createJson() { + var bookData = {} + bookData.type = 'book' + /* Set corresponding value if the input is not empty */ + if(bookURLState !== '') {bookData.book_url = bookURLState} + if(bookTitleState !== '') {bookData.title = bookTitleState} + if(bookIDState !== '') {bookData.id = bookIDState} + if(bookISBNState !== '') {bookData.ISBN = bookISBNState} + if(bookAuthorURLState !== '') {bookData.author_url = bookAuthorURLState} + if(bookAuthorState !== '') {bookData.author = bookAuthorState} + if(bookRatingState !== '') {bookData.rating = parseFloat(bookRatingState)} + if(bookRatingCountState !== '') {bookData.rating_count = parseInt(bookRatingCountState)} + if(bookReviewCountState !== '') {bookData.review_count = parseInt(bookReviewCountState)} + if(bookSimilarBooksState !== '') {bookData.similar_books = bookSimilarBooksState.split(', ')} + setDataState(bookData) + return bookData + } + + /** Function used in PUT and POST: extract user input of authorData from the form and parse into JSON */ + function createJsonAuthor() { + var authorData = {} + authorData.type = 'author' + /* Set corresponding value if the input is not empty */ + if(authorNameState !== '') {authorData.name = authorNameState} + if(authorURLState !== '') {authorData.author_url = authorURLState} + if(authorIDState !== '') {authorData.id = authorIDState} + if(authorRatingState !== '') {authorData.rating = parseFloat(authorRatingState)} + if(authorRatingCountState !== '') {authorData.rating_count = parseInt(authorRatingCountState)} + if(authorReviewCountState !== '') {authorData.review_count = parseInt(authorReviewCountState)} + if(authorImageURLState !== '') {authorData.image_url = authorImageURLState} + if(authorRelatedAuthorsState !== '') {authorData.related_authors = authorRelatedAuthorsState.split(', ')} + if(authorAuthorBooksState !== '') {authorData.author_books = authorAuthorBooksState.split(',')} + setDataState(authorData) + return authorData + } + + /** Function used to hide the input form */ + function hideForm() { + setFormState('hide') + } + + /** Function used to show bookData input form */ + function changeFormStateBook() { + setRenderState(false) + if (formState === 'hide') { + setFormState('showBook') + } else { + setFormState('hide') + } + } + + /** Function used to show authorData input form */ + function changeFormStateAuthor() { + setRenderState(false) + if (formState === 'hide') { + setFormState('showAuthor') + } else { + setFormState('hide') + } + } + + /** Function used to set Dialog visible, which is called when a response is received */ + function showDialog() { + setDialogState(true) + } + + /** Function used in PUT requests */ + function put() { + setRenderState(false) + hideForm() + var data; + if (queryStr.includes('book')) { + data = createJson() + } else { + data = createJsonAuthor() + } + let config = { + headers: { + 'Content-Type': 'application/json', + } + } + axios.put('http://127.0.0.1:5000/api/' + queryStr, data, config) + .then(function (response) { + // handle success + setSign(CorrectSign) + showDialog() + document.getElementById('dialog').value = response.status + ":\n" + response.statusText + }) + .catch(function (error) { + // handle error + setSign(ErrorSign) + showDialog() + document.getElementById('dialog').value = error //.response.status + ":\n" + error.response.statusText + }) + } + + /** Function used in POST requests */ + function post() { + setRenderState(false) + let config = { + headers: { + 'Content-Type': 'application/json', + } + } + hideForm() + var data; + if (queryStr.includes('book')) { + data = createJson() + } else { + data = createJsonAuthor() + } + axios.post('http://127.0.0.1:5000/api/' + queryStr, JSON.stringify(data), config) + .then(function (response) { + // handle success + setSign(CorrectSign) + showDialog() + document.getElementById('dialog').value = response.status + ":\n" + response.statusText + }) + .catch(function (error) { + // handle error + setSign(ErrorSign); + showDialog(); + document.getElementById('dialog').value = error //.response.status + ":\n" + error.response.statusText + }) + } + + /** Function used in DELETE requests */ + function delete_data() { + setRenderState(false) + axios.delete('http://127.0.0.1:5000/api/' + queryStr) + .then(function (response) { + // handle success + setSign(CorrectSign) + showDialog() + document.getElementById('dialog').value = response.status + ":\n" + response.statusText + }) + .catch(function (error) { + // handle error + setSign(ErrorSign) + showDialog() + document.getElementById('dialog').value = error //.response.status + ":\n" + error.response.statusText + }) + } + + /** HTML code */ + return ( + <div> + {/* Dialog used when received response from server */} + {dialogState === true && + <Dialog //Warning, not responsive! + show={dialogState} + onOutsideClick={() => setDialogState(false)}> + <div style={{marginTop:'20%'}}> + <div style={{ + marginLeft: '35%' + }}> + <img alt='responseStatus' src={sign}/> + </div> + <div> + <textarea id='dialog' + readOnly={true} + rows="3" + cols="38" + style={{ + border:'2px solid silver', + backgroundColor: 'white', + fontSize: '200%', + margin: '50px' + }}> + {text} + </textarea> + </div> + </div> + </Dialog>} + <h1 style={center}> + Welcome to the home page of GoodReads Crawler! </h1> + <h3 style={center}> Please input your query string:</h3> + <div style={center}> + <input + id='queryString' + type='text' + placeholder='example: book/?id=12345678' + size='40' + value={queryStr} + onChange={(e) => setQueryStr(e.target.value)} + /> + </div> + <h3 style={center}> Please input your data for uploading (only effective for POST and PUT):</h3> + {/* Buttons used to select book or author data to be passed to PUT/POST */} + {formState === 'hide' && + <div style={center}> + <button style={{width:60}} onClick={changeFormStateBook}> + book + </button> + <button style={{marginLeft:10, width:60}} onClick={changeFormStateAuthor}> + author + </button> + </div>} + {/* Form-like input for bookData */} + {formState === 'showBook' && + <form style={{border:'2px solid silver', marginLeft:'180px', marginRight: '180px'}}> + <div style={center}> + <input id='bookURL' style={center} type='text' size='40' value={bookURLState} placeholder='bookURL' onChange={(e) => setBookURLState(e.target.value)}/> + </div> + <div style={center}> + <input id='bookTtile' style={center} type='text' size='40' value={bookTitleState} placeholder='bookTtile' onChange={(e) => setBookTitleState(e.target.value)}/> + </div> + <div style={center}> + <input id='bookID' type='text' size='40' value={bookIDState} placeholder='bookID' onChange={(e) => setBookIDState(e.target.value)}/> + </div> + <div style={center}> + <input id='bookISBN' type='text' size='40' value={bookISBNState} placeholder='bookISBN' onChange={(e) => setBookISBNState(e.target.value)}/> + </div> + <div style={center}> + <input id='bookAuthorURL' type='text' size='40' value={bookAuthorURLState} placeholder='bookAuthorURL' onChange={(e) => setBookAuthorURLState(e.target.value)}/> + </div> + <div style={center}> + <input id='bookAuthor' type='text' size='40' value={bookAuthorState} placeholder='bookAuthor' onChange={(e) => setBookAuthorState(e.target.value)}/> + </div> + <div style={center}> + <input id='bookRating' type='text' size='40' value={bookRatingState} placeholder='bookRating' onChange={(e) => setBookRatingState(e.target.value)}/> + </div> + <div style={center}> + <input id='bookRatingCount' type='text' size='40' value={bookRatingCountState} placeholder='bookRatingCount' onChange={(e) => setBookRatingCountState(e.target.value)}/> + </div> + <div style={center}> + <input id='bookReviewCount' type='text' size='40' value={bookReviewCountState} placeholder='bookReviewCount' onChange={(e) => setBookReviewCountState(e.target.value)}/> + </div> + <div style={center}> + <input id='bookImageURL' type='text' size='40' value={bookImageURLState} placeholder='bookImageURL' onChange={(e) => setBookImageURLState(e.target.value)}/> + </div> + <div style={center}> + <input id='bookSimilarBooks' type='text' size='40' value={bookSimilarBooksState} placeholder='bookSimilarBooks' onChange={(e) => setBookSimilarBooksState(e.target.value)}/> + </div> + </form> + } + {/* Form-like input for authorData */} + {formState === 'showAuthor' && + <form style={{border:'2px solid silver', marginLeft:'180px', marginRight: '180px'}}> + <div style={center}> + <input id='authorName' style={center} type='text' size='40' value={authorNameState} placeholder='authorName' onChange={(e) => setAuthorNameState(e.target.value)}/> + </div> + <div style={center}> + <input id='authorURL' style={center} type='text' size='40' value={authorURLState} placeholder='authorURL' onChange={(e) => setAuthorURLState(e.target.value)}/> + </div> + <div style={center}> + <input id='authorID' type='text' size='40' value={authorIDState} placeholder='authorID' onChange={(e) => setAuthorIDState(e.target.value)}/> + </div> + <div style={center}> + <input id='authorRating' type='text' size='40' value={authorRatingState} placeholder='authorRating' onChange={(e) => setAuthorRatingState(e.target.value)}/> + </div> + <div style={center}> + <input id='authorRatingCount' type='text' size='40' value={authorRatingCountState} placeholder='authorRatingCount' onChange={(e) => setAuthorRatingCountState(e.target.value)}/> + </div> + <div style={center}> + <input id='authorReviewCount' type='text' size='40' value={authorReviewCountState} placeholder='authorReviewCount' onChange={(e) => setAuthorReviewCountState(e.target.value)}/> + </div> + <div style={center}> + <input id='authorImageURL' type='text' size='40' value={authorImageURLState} placeholder='authorImageURL' onChange={(e) => setAuthorImageURLState(e.target.value)}/> + </div> + <div style={center}> + <input id='authorRelatedAuthors' type='text' size='40' value={authorRelatedAuthorsState} placeholder='authorRelatedAuthors' onChange={(e) => setAuthorRelatedAuthorsState(e.target.value)}/> + </div> + <div style={center}> + <input id='authorBooks' type='text' size='40' value={authorAuthorBooksState} placeholder='authorBooks' onChange={(e) => setAuthorAuthorBooksState(e.target.value)}/> + </div> + </form> + } + {formState !== 'hide' && + <button style={{marginLeft:600, width:60}} onClick={hideForm}> back </button> + } + <div style={{marginTop:20, display: 'flex', justifyContent: 'center', alignItems: 'center'}}> + {/* Fout buttons for GET PUT POST and DELETE */} + <FourButtons + backColor='blue' + borderColor='RoyalBlue' + leftMargin={0} + text='GET' + func={get} /> + <FourButtons + backColor='black' + borderColor='gray' + leftMargin={20} + text='PUT' + func={put} /> + <FourButtons + backColor='green' + borderColor='seagreen' + leftMargin={20} + text='POST' + func={post} /> + <FourButtons + backColor='red' + borderColor='indianred' + leftMargin={20} + text='DELETE' + func={delete_data} /> + </div> + {renderState === true && + <div> + {/* Data Table used to render response json data */} + <ElementDisplay data={dataState}/> + </div>} + </div> + ); +} + +export default App; \ No newline at end of file diff --git a/web/app/src/App.test.js b/web/app/src/App.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1f03afeece5ac28064fa3c73a29215037465f789 --- /dev/null +++ b/web/app/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(<App />); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/web/app/src/Components/ElementDisplay.js b/web/app/src/Components/ElementDisplay.js new file mode 100644 index 0000000000000000000000000000000000000000..a8d53ad28d9a3f4119ce123dba91f21eb2ae6eaf --- /dev/null +++ b/web/app/src/Components/ElementDisplay.js @@ -0,0 +1,25 @@ +import MUIDataTable from "mui-datatables"; + +/** MUI dataTable component, used for rendering received response */ +const ElementDisplay = (props) => { + + var fields = Object.keys(props.data) + var values = Object.values(props.data) + console.log(Object.values(props.data)) + console.log(Object.keys(props.data)) + const columns = fields + const data = [values] + const options = { + 'responsive':'vertical' + } + return ( + <MUIDataTable + title={"GET Data"} + data={data} + columns={columns} + options={options} + /> + ) +} + +export default ElementDisplay diff --git a/web/app/src/Components/FourButtons.js b/web/app/src/Components/FourButtons.js new file mode 100644 index 0000000000000000000000000000000000000000..e51eb2c582fc27602beb94d57e0c29e37d672e86 --- /dev/null +++ b/web/app/src/Components/FourButtons.js @@ -0,0 +1,21 @@ +/** Button Component for GET PUT POST DELETE */ +const FourButtons = ({backColor, borderColor, leftMargin, text, func}) => { + return ( + <button + style={{ + width:70, + height:30, + backgroundColor:backColor, + color:'white', + marginLeft:leftMargin, + borderLeftWidth:4, + borderTopWidth:3, + borderColor:borderColor, + borderBottomWidth:3}} + className='btn' + onClick={func}> {text} + </button> + ) +} + +export default FourButtons diff --git a/web/app/src/index.css b/web/app/src/index.css new file mode 100644 index 0000000000000000000000000000000000000000..ec2585e8c0bb8188184ed1e0703c4c8f2a8419b0 --- /dev/null +++ b/web/app/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/web/app/src/index.js b/web/app/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ef2edf8ea3fc42258464231e29140c8723458c1e --- /dev/null +++ b/web/app/src/index.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; +import reportWebVitals from './reportWebVitals'; + +ReactDOM.render( + <React.StrictMode> + <App /> + </React.StrictMode>, + document.getElementById('root') +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/web/app/src/material/sign_correct.png b/web/app/src/material/sign_correct.png new file mode 100644 index 0000000000000000000000000000000000000000..f4c62915fc0b2c590da99790c4369a3e73f75cfe Binary files /dev/null and b/web/app/src/material/sign_correct.png differ diff --git a/web/app/src/material/sign_error.png b/web/app/src/material/sign_error.png new file mode 100644 index 0000000000000000000000000000000000000000..52c8cb30694f970a86ed6baf14741e1a03183ca1 Binary files /dev/null and b/web/app/src/material/sign_error.png differ diff --git a/web/app/src/reportWebVitals.js b/web/app/src/reportWebVitals.js new file mode 100644 index 0000000000000000000000000000000000000000..5253d3ad9e6be6690549cb255f5952337b02401d --- /dev/null +++ b/web/app/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/web/app/src/setupTests.js b/web/app/src/setupTests.js new file mode 100644 index 0000000000000000000000000000000000000000..8f2609b7b3e0e3897ab3bcaad13caf6876e48699 --- /dev/null +++ b/web/app/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom';