mirror of
https://github.com/tiyn/container-critique.git
synced 2025-04-02 16:17:49 +02:00
search: added full-text search and docstrings
This commit is contained in:
parent
a6e1735cac
commit
284a597d4a
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
data
|
data
|
||||||
data.db
|
data.db
|
||||||
|
indexdir
|
||||||
|
137
src/app.py
137
src/app.py
@ -1,14 +1,17 @@
|
|||||||
from flask import Flask, flash, render_template, redirect, abort, url_for
|
from flask import Flask, flash, render_template, redirect, abort, url_for, \
|
||||||
|
request
|
||||||
from flask_ckeditor import CKEditor
|
from flask_ckeditor import CKEditor
|
||||||
from flask_login import current_user, login_user, LoginManager, logout_user, \
|
from flask_login import current_user, login_user, LoginManager, logout_user, \
|
||||||
login_required
|
login_required
|
||||||
from flask_wtf import CSRFProtect
|
from flask_wtf import CSRFProtect
|
||||||
import os
|
import os
|
||||||
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from content import rating_to_star
|
from content import rating_to_star
|
||||||
from database import Database
|
from database import Database
|
||||||
from forms import LoginForm, RegisterForm, WriteForm
|
from forms import LoginForm, RegisterForm, WriteForm, SearchForm
|
||||||
|
from search import create_search_index, ft_search
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@ -23,20 +26,55 @@ login = LoginManager(app)
|
|||||||
login.login_view = "login"
|
login.login_view = "login"
|
||||||
|
|
||||||
|
|
||||||
|
@login.user_loader
|
||||||
|
def load_user(ident):
|
||||||
|
"""
|
||||||
|
Returns a user by id.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ident(int): id of the user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User: user that matches the id, None if none matches the id
|
||||||
|
"""
|
||||||
|
user = db.get_user_by_id(ident)
|
||||||
|
if user is not None:
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_title():
|
def inject_title():
|
||||||
return dict(title=config.TITLE, style=config.STYLE, \
|
"""
|
||||||
description=config.DESCRIPTION, \
|
Injects variables to the jinja2 templates.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: dictionary of variables to inject.
|
||||||
|
"""
|
||||||
|
return dict(title=config.TITLE, style=config.STYLE,
|
||||||
|
description=config.DESCRIPTION,
|
||||||
registration=config.ALLOW_REGISTRATION, r_to_star=rating_to_star)
|
registration=config.ALLOW_REGISTRATION, r_to_star=rating_to_star)
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(HTTPException)
|
||||||
def page_not_found(e):
|
def page_not_found(e):
|
||||||
return render_template("error.html", errorcode="404"), 404
|
"""
|
||||||
|
Renders the error pages.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: html formatted Error page
|
||||||
|
"""
|
||||||
|
return render_template("error.html", errorcode=str(e.code) + " " + e.name), e.code
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
|
"""
|
||||||
|
Renders the index page.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: html formatted index page
|
||||||
|
"""
|
||||||
entries = db.get_entries()
|
entries = db.get_entries()
|
||||||
entries.reverse()
|
entries.reverse()
|
||||||
return render_template("index.html", entries=entries)
|
return render_template("index.html", entries=entries)
|
||||||
@ -44,6 +82,12 @@ def index():
|
|||||||
|
|
||||||
@app.route("/archive")
|
@app.route("/archive")
|
||||||
def archive():
|
def archive():
|
||||||
|
"""
|
||||||
|
Renders the archive page.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: html formatted archive page
|
||||||
|
"""
|
||||||
entries = db.get_entries()
|
entries = db.get_entries()
|
||||||
entries.sort(key=lambda y: y.item.name)
|
entries.sort(key=lambda y: y.item.name)
|
||||||
entries.reverse()
|
entries.reverse()
|
||||||
@ -54,7 +98,16 @@ def archive():
|
|||||||
|
|
||||||
@app.route("/user/<name>")
|
@app.route("/user/<name>")
|
||||||
def user(name):
|
def user(name):
|
||||||
entries = db.get_entries_by_user(name)
|
"""
|
||||||
|
Renders the user page of a specific user.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name(str): name of the user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: html formatted user page
|
||||||
|
"""
|
||||||
|
entries = db.get_entries_by_username(name)
|
||||||
entries.sort(key=lambda y: y.item.name)
|
entries.sort(key=lambda y: y.item.name)
|
||||||
entries.reverse()
|
entries.reverse()
|
||||||
entries.sort(key=lambda y: y.item.date)
|
entries.sort(key=lambda y: y.item.date)
|
||||||
@ -66,6 +119,15 @@ def user(name):
|
|||||||
|
|
||||||
@app.route("/entry/<ident>")
|
@app.route("/entry/<ident>")
|
||||||
def entry(ident):
|
def entry(ident):
|
||||||
|
"""
|
||||||
|
Renders the entry page of a specific entry.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ident(str): ident of the entry
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: html formatted entry page
|
||||||
|
"""
|
||||||
entry = db.get_entry_by_id(ident)
|
entry = db.get_entry_by_id(ident)
|
||||||
if entry is not None:
|
if entry is not None:
|
||||||
return render_template("standalone.html", entry=entry)
|
return render_template("standalone.html", entry=entry)
|
||||||
@ -74,22 +136,43 @@ def entry(ident):
|
|||||||
|
|
||||||
@app.route("/feed")
|
@app.route("/feed")
|
||||||
def feed():
|
def feed():
|
||||||
|
"""
|
||||||
|
Renders the rss feed of a the feed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: xml formatted feed
|
||||||
|
"""
|
||||||
entries = db.get_entries()
|
entries = db.get_entries()
|
||||||
entries.reverse()
|
entries.reverse()
|
||||||
rss_xml = render_template("rss.xml", entries=entries)
|
rss_xml = render_template("rss.xml", entries=entries)
|
||||||
return rss_xml
|
return rss_xml
|
||||||
|
|
||||||
|
|
||||||
@login.user_loader
|
@app.route("/search", methods=["GET", "POST"])
|
||||||
def load_user(ident):
|
def search():
|
||||||
user = db.get_user_by_id(ident)
|
"""
|
||||||
if user is not None:
|
Renders the search page.
|
||||||
return user
|
|
||||||
return None
|
Returns:
|
||||||
|
str: html formatted search page.
|
||||||
|
"""
|
||||||
|
form = SearchForm()
|
||||||
|
if request.method == "POST":
|
||||||
|
query_str = request.form["query_str"]
|
||||||
|
query_res = ft_search(query_str)
|
||||||
|
return render_template("search.html", form=form, results=query_res), 200
|
||||||
|
return render_template("search.html", form=form, content=""), 200
|
||||||
|
|
||||||
|
|
||||||
@app.route("/login", methods=["GET", "POST"])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
|
"""
|
||||||
|
Logs the user in.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: html formatted login page, if login is successful renders the index
|
||||||
|
page.
|
||||||
|
"""
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
form = LoginForm()
|
form = LoginForm()
|
||||||
@ -107,12 +190,25 @@ def login():
|
|||||||
|
|
||||||
@app.route('/logout')
|
@app.route('/logout')
|
||||||
def logout():
|
def logout():
|
||||||
|
"""
|
||||||
|
Logs out the current user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: html formatted index page.
|
||||||
|
"""
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/register", methods=["GET", "POST"])
|
@app.route("/register", methods=["GET", "POST"])
|
||||||
def register():
|
def register():
|
||||||
|
"""
|
||||||
|
Registers new users.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: html formatted registration page, if registration is successful
|
||||||
|
renders the index page.
|
||||||
|
"""
|
||||||
if current_user.is_authenticated or not config.ALLOW_REGISTRATION:
|
if current_user.is_authenticated or not config.ALLOW_REGISTRATION:
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
form = RegisterForm()
|
form = RegisterForm()
|
||||||
@ -132,12 +228,20 @@ def register():
|
|||||||
@app.route("/write_entry", methods=["GET", "POST"])
|
@app.route("/write_entry", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def write_entry():
|
def write_entry():
|
||||||
|
"""
|
||||||
|
Stores newly written entries.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: html formatted write entry page, if posting of the entry is successful
|
||||||
|
renders the index page.
|
||||||
|
"""
|
||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
form = WriteForm()
|
form = WriteForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
db.insert_entry(form.name.data, form.date.data,
|
db.insert_entry(form.name.data, form.date.data,
|
||||||
form.text.data, form.rating.data, current_user.id)
|
form.text.data, form.rating.data, current_user.id)
|
||||||
|
create_search_index()
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
return render_template("write.html", form=form)
|
return render_template("write.html", form=form)
|
||||||
|
|
||||||
@ -145,10 +249,17 @@ def write_entry():
|
|||||||
@app.route("/delete_entry/<ident>", methods=["GET", "POST"])
|
@app.route("/delete_entry/<ident>", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def delete_entry(ident):
|
def delete_entry(ident):
|
||||||
|
"""
|
||||||
|
Deletes an existing entry.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: html formatted index entry page.
|
||||||
|
"""
|
||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
if current_user.id == db.get_entry_by_id(ident).user.id:
|
if current_user.id == db.get_entry_by_id(ident).user.id:
|
||||||
db.delete_entry(ident)
|
db.delete_entry(ident)
|
||||||
|
create_search_index()
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,3 +12,9 @@ STYLE = "dark"
|
|||||||
|
|
||||||
# Allow new registrations
|
# Allow new registrations
|
||||||
ALLOW_REGISTRATION = True
|
ALLOW_REGISTRATION = True
|
||||||
|
|
||||||
|
# Location of the search-indexing directory
|
||||||
|
INDEX_DIR = "indexdir"
|
||||||
|
|
||||||
|
# Number of search results to show
|
||||||
|
SEARCH_NUMBER = 10
|
||||||
|
@ -8,10 +8,10 @@ def rating_to_star(rating):
|
|||||||
Creates a string with stars based on the rating.
|
Creates a string with stars based on the rating.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
rating: rating with minimum of 0 and maximum of 100.
|
rating: rating with minimum of 0 and maximum of 100
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
string: unicode-formatted star-rating string.
|
string: unicode-formatted star-rating string
|
||||||
"""
|
"""
|
||||||
res = u"\u272D"*int(rating/20)
|
res = u"\u272D"*int(rating/20)
|
||||||
length = len(res)
|
length = len(res)
|
||||||
|
259
src/database.py
259
src/database.py
@ -5,6 +5,17 @@ from werkzeug.security import generate_password_hash, check_password_hash
|
|||||||
|
|
||||||
|
|
||||||
class User():
|
class User():
|
||||||
|
"""
|
||||||
|
A class to represent a user.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name (str): name of the user
|
||||||
|
id (int): id of the user
|
||||||
|
is_active (bool): check if the user is active
|
||||||
|
is_authenticated (bool): check if the user is logged in
|
||||||
|
is_anonymous (bool): check if the user is is_anonymous
|
||||||
|
pass_hash (str): hash of the users password
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, name, pass_hash=None):
|
def __init__(self, name, pass_hash=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
@ -15,19 +26,61 @@ class User():
|
|||||||
self.pass_hash = pass_hash
|
self.pass_hash = pass_hash
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
|
"""
|
||||||
|
Set the password hash of the user from a password.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
password (str): password to add to the user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
self.pass_hash = generate_password_hash(password)
|
self.pass_hash = generate_password_hash(password)
|
||||||
|
|
||||||
def set_id(self, ident):
|
def set_id(self, ident):
|
||||||
|
"""
|
||||||
|
Set the id of the user.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
id (str): id to add to the user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
self.id = ident
|
self.id = ident
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
|
"""
|
||||||
|
Check if a given password matches the one of the users by comparing the
|
||||||
|
hashes.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
password (str): password to compare the users password to
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if it matches the users password, False otherwise
|
||||||
|
"""
|
||||||
return check_password_hash(self.pass_hash, password)
|
return check_password_hash(self.pass_hash, password)
|
||||||
|
|
||||||
def get_id(self):
|
def get_id(self):
|
||||||
|
"""
|
||||||
|
Get the id of the user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: id of the user
|
||||||
|
"""
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
|
|
||||||
class Item():
|
class Item():
|
||||||
|
"""
|
||||||
|
A class to represent an item.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name (str): name of the item
|
||||||
|
id (int): id of the item
|
||||||
|
date (str): date the item was created
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, name, date):
|
def __init__(self, name, date):
|
||||||
self.name = name
|
self.name = name
|
||||||
@ -35,29 +88,73 @@ class Item():
|
|||||||
self.id = None
|
self.id = None
|
||||||
|
|
||||||
def set_id(self, ident):
|
def set_id(self, ident):
|
||||||
|
"""
|
||||||
|
Set the id of the item.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: id of the item
|
||||||
|
"""
|
||||||
self.id = ident
|
self.id = ident
|
||||||
|
|
||||||
|
|
||||||
class Entry():
|
class Entry():
|
||||||
|
"""
|
||||||
|
A class to represent an entry.
|
||||||
|
|
||||||
def __init__(self, text, rating, reviewed):
|
Attributes:
|
||||||
|
text (str): text of the entry
|
||||||
|
rating (int): rating of the item
|
||||||
|
date (str): date the entry was created
|
||||||
|
item (Item): item that is referenced by the entry
|
||||||
|
user (User): user that authored the entry
|
||||||
|
id (int): id of the item
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text, rating, date):
|
||||||
self.text = text
|
self.text = text
|
||||||
self.rating = rating
|
self.rating = rating
|
||||||
self.reviewed = reviewed
|
self.date = date
|
||||||
self.item = None
|
self.item = None
|
||||||
self.user = None
|
self.user = None
|
||||||
|
|
||||||
def set_id(self, ident):
|
def set_id(self, ident):
|
||||||
|
"""
|
||||||
|
Set the id of the entry.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ident(int): id of the entry
|
||||||
|
"""
|
||||||
self.id = ident
|
self.id = ident
|
||||||
|
|
||||||
def set_item(self, item):
|
def set_item(self, item):
|
||||||
|
"""
|
||||||
|
Set the item of the entry.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
item(Item): item of the entry
|
||||||
|
"""
|
||||||
self.item = item
|
self.item = item
|
||||||
|
|
||||||
def set_user(self, user):
|
def set_user(self, user):
|
||||||
|
"""
|
||||||
|
Set the user of the entry.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user(User): user of the entry
|
||||||
|
"""
|
||||||
self.user = user
|
self.user = user
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
|
"""
|
||||||
|
A class to represent an entry.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
USER_TABLE_FILE (str): name of the user table
|
||||||
|
ENTRY_TABLE_FILE (str): name of the entry table
|
||||||
|
ITEM_TABLE_FILE (str): name of the item table
|
||||||
|
DB_DIR(PathLike): path that leads to the directory containing the database
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.USER_TABLE_FILE = 'USERS'
|
self.USER_TABLE_FILE = 'USERS'
|
||||||
@ -70,12 +167,17 @@ class Database:
|
|||||||
"""
|
"""
|
||||||
Connect to an existing database instance based on the object
|
Connect to an existing database instance based on the object
|
||||||
attributes.
|
attributes.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Connection: connection to the database
|
||||||
"""
|
"""
|
||||||
path = os.path.join(self.DB_DIR, "data.db")
|
path = os.path.join(self.DB_DIR, "data.db")
|
||||||
return sqlite3.connect(path)
|
return sqlite3.connect(path)
|
||||||
|
|
||||||
def setup_db(self):
|
def setup_db(self):
|
||||||
"""Creates a database with tables."""
|
"""
|
||||||
|
Creates a database with the needed tables if it doesn't already exits.
|
||||||
|
"""
|
||||||
db = self.connect()
|
db = self.connect()
|
||||||
crs = db.cursor()
|
crs = db.cursor()
|
||||||
query = "CREATE TABLE IF NOT EXISTS " + self.USER_TABLE_FILE + \
|
query = "CREATE TABLE IF NOT EXISTS " + self.USER_TABLE_FILE + \
|
||||||
@ -95,11 +197,21 @@ class Database:
|
|||||||
"text TEXT NOT NULL," + \
|
"text TEXT NOT NULL," + \
|
||||||
"rating INTEGER NOT NULL," +\
|
"rating INTEGER NOT NULL," +\
|
||||||
"user_id INTEGER REFERENCES " + self.USER_TABLE_FILE + "(id),"\
|
"user_id INTEGER REFERENCES " + self.USER_TABLE_FILE + "(id),"\
|
||||||
"reviewed CHAR(10) NOT NULL)"
|
"date CHAR(10) NOT NULL)"
|
||||||
crs.execute(query)
|
crs.execute(query)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
def insert_user(self, username, password):
|
def insert_user(self, username, password):
|
||||||
|
"""
|
||||||
|
Insert a row in the user table.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
username (str): name of the user to add
|
||||||
|
password (str): password of the user to add
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: number of the line the row was added, None if it wasn't successful
|
||||||
|
"""
|
||||||
pass_hash = generate_password_hash(password)
|
pass_hash = generate_password_hash(password)
|
||||||
if self.get_user_by_name(username) is None and pass_hash is not None:
|
if self.get_user_by_name(username) is None and pass_hash is not None:
|
||||||
db = self.connect()
|
db = self.connect()
|
||||||
@ -113,6 +225,19 @@ class Database:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def insert_entry(self, name, date, text, rating, user_id=None):
|
def insert_entry(self, name, date, text, rating, user_id=None):
|
||||||
|
"""
|
||||||
|
Insert a row in the entry table.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name (str): name of the entry to add
|
||||||
|
date (str): date of the entry to add
|
||||||
|
text (str): text of the entry to add
|
||||||
|
rating (str): rating of the entry to add
|
||||||
|
user_id (int): id of the user referenced by the entry to add
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: number of the line the row was added
|
||||||
|
"""
|
||||||
db = self.connect()
|
db = self.connect()
|
||||||
crs = db.cursor()
|
crs = db.cursor()
|
||||||
query = "INSERT OR IGNORE INTO " + self.ITEM_TABLE_FILE + \
|
query = "INSERT OR IGNORE INTO " + self.ITEM_TABLE_FILE + \
|
||||||
@ -122,15 +247,24 @@ class Database:
|
|||||||
" WHERE name = ? AND date = ?"
|
" WHERE name = ? AND date = ?"
|
||||||
crs.execute(query, (name, date))
|
crs.execute(query, (name, date))
|
||||||
item_id = crs.fetchone()[0]
|
item_id = crs.fetchone()[0]
|
||||||
reviewed = dt.today().strftime('%Y-%m-%d')
|
date = dt.today().strftime('%Y-%m-%d')
|
||||||
query = "INSERT INTO " + self.ENTRY_TABLE_FILE + \
|
query = "INSERT INTO " + self.ENTRY_TABLE_FILE + \
|
||||||
"(`item_id`, `text`, `rating`, `user_id`, `reviewed`)" + \
|
"(`item_id`, `text`, `rating`, `user_id`, `date`)" + \
|
||||||
"VALUES (?, ?, ?, ?, ?)"
|
"VALUES (?, ?, ?, ?, ?)"
|
||||||
crs.execute(query, (item_id, text, rating, user_id, reviewed))
|
crs.execute(query, (item_id, text, rating, user_id, date))
|
||||||
db.commit()
|
db.commit()
|
||||||
return crs.lastrowid
|
return crs.lastrowid
|
||||||
|
|
||||||
def delete_entry(self, ident):
|
def delete_entry(self, ident):
|
||||||
|
"""
|
||||||
|
Delete a row from the entry table based on the entrys id.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ident (int): id of the entry to remove
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: number of the line the row was removed from
|
||||||
|
"""
|
||||||
db = self.connect()
|
db = self.connect()
|
||||||
crs = db.cursor()
|
crs = db.cursor()
|
||||||
query = "DELETE FROM " + self.ENTRY_TABLE_FILE + " WHERE id = ?"
|
query = "DELETE FROM " + self.ENTRY_TABLE_FILE + " WHERE id = ?"
|
||||||
@ -139,16 +273,31 @@ class Database:
|
|||||||
return crs.lastrowid
|
return crs.lastrowid
|
||||||
|
|
||||||
def get_entries(self):
|
def get_entries(self):
|
||||||
|
"""
|
||||||
|
Return all the entries stored in the database.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
List(Entry): list of entries in database
|
||||||
|
"""
|
||||||
db = self.connect()
|
db = self.connect()
|
||||||
crs = db.cursor()
|
crs = db.cursor()
|
||||||
query = "SELECT * FROM " + self.ENTRY_TABLE_FILE
|
query = "SELECT * FROM " + self.ENTRY_TABLE_FILE
|
||||||
crs.execute(query)
|
crs.execute(query)
|
||||||
res = []
|
res = []
|
||||||
for item in crs.fetchall():
|
for item in crs.fetchall():
|
||||||
res.append(self.db_to_entry(*item))
|
res.append(self.entry_from_db(*item))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_entry_by_id(self, ident):
|
def get_entry_by_id(self, ident):
|
||||||
|
"""
|
||||||
|
Return an entry stored in the database based on the entrys id.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ident (int): id of the entry to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Entry: entry that matched the given id
|
||||||
|
"""
|
||||||
db = self.connect()
|
db = self.connect()
|
||||||
crs = db.cursor()
|
crs = db.cursor()
|
||||||
query = "SELECT * FROM " + self.ENTRY_TABLE_FILE + " WHERE id = ?"
|
query = "SELECT * FROM " + self.ENTRY_TABLE_FILE + " WHERE id = ?"
|
||||||
@ -157,21 +306,39 @@ class Database:
|
|||||||
if fetched is None:
|
if fetched is None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return self.db_to_entry(*fetched)
|
return self.entry_from_db(*fetched)
|
||||||
|
|
||||||
def get_entries_by_user(self, name):
|
def get_entries_by_username(self, username):
|
||||||
|
"""
|
||||||
|
Return a entries stored in the database based on the entries name.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
username (str): name of the entries to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List(Entry): entries that matched the given name
|
||||||
|
"""
|
||||||
db = self.connect()
|
db = self.connect()
|
||||||
crs = db.cursor()
|
crs = db.cursor()
|
||||||
query = "SELECT * FROM " + self.ENTRY_TABLE_FILE + \
|
query = "SELECT * FROM " + self.ENTRY_TABLE_FILE + \
|
||||||
" WHERE user_id = (SELECT id FROM " + self.USER_TABLE_FILE + \
|
" WHERE user_id = (SELECT id FROM " + self.USER_TABLE_FILE + \
|
||||||
" WHERE name = ?)"
|
" WHERE name = ?)"
|
||||||
crs.execute(query, (name, ))
|
crs.execute(query, (username, ))
|
||||||
res = []
|
res = []
|
||||||
for item in crs.fetchall():
|
for item in crs.fetchall():
|
||||||
res.append(self.db_to_entry(*item))
|
res.append(self.entry_from_db(*item))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_item_by_id(self, ident):
|
def get_item_by_id(self, ident):
|
||||||
|
"""
|
||||||
|
Return an item stored in the database based on the items id.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ident (int): id of the item to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Item: item that matched the given id
|
||||||
|
"""
|
||||||
db = self.connect()
|
db = self.connect()
|
||||||
crs = db.cursor()
|
crs = db.cursor()
|
||||||
query = "SELECT * FROM " + self.ITEM_TABLE_FILE + " WHERE id = ?"
|
query = "SELECT * FROM " + self.ITEM_TABLE_FILE + " WHERE id = ?"
|
||||||
@ -180,9 +347,18 @@ class Database:
|
|||||||
if fetched is None:
|
if fetched is None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return self.db_to_item(*fetched)
|
return self.item_from_db(*fetched)
|
||||||
|
|
||||||
def get_user_by_id(self, ident):
|
def get_user_by_id(self, ident):
|
||||||
|
"""
|
||||||
|
Return a user stored in the database based on the users id.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ident (int): id of the user to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Item: user that matched the given id
|
||||||
|
"""
|
||||||
db = self.connect()
|
db = self.connect()
|
||||||
crs = db.cursor()
|
crs = db.cursor()
|
||||||
query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE id = ?"
|
query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE id = ?"
|
||||||
@ -191,9 +367,18 @@ class Database:
|
|||||||
if fetched is None:
|
if fetched is None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return self.db_to_user(*fetched)
|
return self.user_from_db(*fetched)
|
||||||
|
|
||||||
def get_user_by_name(self, name):
|
def get_user_by_name(self, name):
|
||||||
|
"""
|
||||||
|
Return a user stored in the database based on the user name.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name (str): name of the user to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Entry: user that matched the given name
|
||||||
|
"""
|
||||||
db = self.connect()
|
db = self.connect()
|
||||||
crs = db.cursor()
|
crs = db.cursor()
|
||||||
query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE name = ?"
|
query = "SELECT * FROM " + self.USER_TABLE_FILE + " WHERE name = ?"
|
||||||
@ -202,20 +387,56 @@ class Database:
|
|||||||
if fetched is None:
|
if fetched is None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return self.db_to_user(*fetched)
|
return self.user_from_db(*fetched)
|
||||||
|
|
||||||
def db_to_user(self, ident, name, pass_hash):
|
def user_from_db(self, ident, name, pass_hash):
|
||||||
|
"""
|
||||||
|
Return a user from given database parameters.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ident: id of the user
|
||||||
|
name: text of the user
|
||||||
|
pass_hash: password hash of the user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User: user element with given variables
|
||||||
|
"""
|
||||||
user = User(name, pass_hash)
|
user = User(name, pass_hash)
|
||||||
user.set_id(ident)
|
user.set_id(ident)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def db_to_item(self, ident, name, date):
|
def item_from_db(self, ident, name, date):
|
||||||
|
"""
|
||||||
|
Return an item from given database parameters.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ident: id of the item
|
||||||
|
name: text of the item
|
||||||
|
date: date of the day the item was created
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Item: entry element with given variables
|
||||||
|
"""
|
||||||
item = Item(name, date)
|
item = Item(name, date)
|
||||||
item.set_id(ident)
|
item.set_id(ident)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def db_to_entry(self, ident, item_id, text, rating, user_id, reviewed):
|
def entry_from_db(self, ident, item_id, text, rating, user_id, date):
|
||||||
entry = Entry(text, rating, reviewed)
|
"""
|
||||||
|
Return an entry from given database parameters.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ident: id of the entry
|
||||||
|
item_id: id of the referenced item
|
||||||
|
text: text of the entry
|
||||||
|
rating: rating of the entry
|
||||||
|
user_id: id of the user that authored the entry
|
||||||
|
date: date of the day the entry was written
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Entry: entry element with given variables
|
||||||
|
"""
|
||||||
|
entry = Entry(text, rating, date)
|
||||||
entry.set_id(ident)
|
entry.set_id(ident)
|
||||||
entry.set_item(self.get_item_by_id(item_id))
|
entry.set_item(self.get_item_by_id(item_id))
|
||||||
entry.set_user(self.get_user_by_id(user_id))
|
entry.set_user(self.get_user_by_id(user_id))
|
||||||
|
32
src/forms.py
32
src/forms.py
@ -1,13 +1,16 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from flask_ckeditor import CKEditorField
|
from flask_ckeditor import CKEditorField
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, PasswordField, SubmitField
|
from wtforms import StringField, PasswordField, SubmitField, TextField
|
||||||
from wtforms.fields.html5 import IntegerField
|
from wtforms.fields.html5 import IntegerField
|
||||||
from wtforms.validators import DataRequired, EqualTo, InputRequired, \
|
from wtforms.validators import DataRequired, EqualTo, InputRequired, \
|
||||||
NumberRange, ValidationError, Length
|
NumberRange, ValidationError, Length
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
class LoginForm(FlaskForm):
|
||||||
|
"""
|
||||||
|
A Class for the Form that is used while logging in.
|
||||||
|
"""
|
||||||
username = StringField("Username", validators=[DataRequired(),
|
username = StringField("Username", validators=[DataRequired(),
|
||||||
Length(min=4, max=32)])
|
Length(min=4, max=32)])
|
||||||
password = PasswordField("Password", validators=[DataRequired(),
|
password = PasswordField("Password", validators=[DataRequired(),
|
||||||
@ -16,6 +19,9 @@ class LoginForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class RegisterForm(FlaskForm):
|
class RegisterForm(FlaskForm):
|
||||||
|
"""
|
||||||
|
A Class for the Form that is used while registering.
|
||||||
|
"""
|
||||||
username = StringField("Username", validators=[DataRequired(),
|
username = StringField("Username", validators=[DataRequired(),
|
||||||
Length(min=4, max=32)])
|
Length(min=4, max=32)])
|
||||||
password = PasswordField("Password", validators=[DataRequired(),
|
password = PasswordField("Password", validators=[DataRequired(),
|
||||||
@ -25,7 +31,19 @@ class RegisterForm(FlaskForm):
|
|||||||
submit = SubmitField("Register")
|
submit = SubmitField("Register")
|
||||||
|
|
||||||
|
|
||||||
|
class SearchForm(FlaskForm):
|
||||||
|
"""
|
||||||
|
A Class for the Form that is used while searching.
|
||||||
|
"""
|
||||||
|
query_str = TextField(
|
||||||
|
"Query", [DataRequired("Please enter the search term")])
|
||||||
|
submit = SubmitField("Search")
|
||||||
|
|
||||||
|
|
||||||
class WriteForm(FlaskForm):
|
class WriteForm(FlaskForm):
|
||||||
|
"""
|
||||||
|
A Class for the Form that is used while writing a new entry.
|
||||||
|
"""
|
||||||
name = StringField("Name", validators=[DataRequired(),
|
name = StringField("Name", validators=[DataRequired(),
|
||||||
Length(min=2, max=64)])
|
Length(min=2, max=64)])
|
||||||
date = IntegerField("Release Year", default=date.today().year, validators=[
|
date = IntegerField("Release Year", default=date.today().year, validators=[
|
||||||
@ -37,5 +55,17 @@ class WriteForm(FlaskForm):
|
|||||||
submit = SubmitField("Publish")
|
submit = SubmitField("Publish")
|
||||||
|
|
||||||
def validate_text(self, text):
|
def validate_text(self, text):
|
||||||
|
"""
|
||||||
|
Validate a given input for html level one headers.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
text (str): text to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidatenError: if the text contains a first level html tag
|
||||||
|
"""
|
||||||
if "<h1>" in text.data or "</h1>" in text.data:
|
if "<h1>" in text.data or "</h1>" in text.data:
|
||||||
raise ValidationError("Headings on level 1 are not permitted.")
|
raise ValidationError("Headings on level 1 are not permitted.")
|
||||||
|
0
src/indexdir/.gitkeep
Normal file
0
src/indexdir/.gitkeep
Normal file
80
src/search.py
Normal file
80
src/search.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from whoosh import scoring
|
||||||
|
from whoosh.index import create_in, open_dir
|
||||||
|
from whoosh.fields import Schema, TEXT, ID
|
||||||
|
from whoosh.qparser import QueryParser
|
||||||
|
|
||||||
|
import config
|
||||||
|
from database import Database
|
||||||
|
|
||||||
|
CLEANR = re.compile('<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});')
|
||||||
|
|
||||||
|
|
||||||
|
def remove_html_tags(text):
|
||||||
|
"""
|
||||||
|
Convert a text from html formatted to unformatted.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
text (str): text to clean
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: text without html tags
|
||||||
|
"""
|
||||||
|
res = re.sub(CLEANR, '', text)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def create_search_index():
|
||||||
|
"""
|
||||||
|
Create the index data to search all entries.
|
||||||
|
"""
|
||||||
|
db = Database()
|
||||||
|
schema = Schema(title=TEXT(stored=True),
|
||||||
|
path=ID(stored=True), content=TEXT(stored=True))
|
||||||
|
if not os.path.exists(config.INDEX_DIR):
|
||||||
|
os.mkdir(config.INDEX_DIR)
|
||||||
|
ix = create_in(config.INDEX_DIR, schema)
|
||||||
|
writer = ix.writer()
|
||||||
|
for entry in db.get_entries():
|
||||||
|
path = str(entry.id)
|
||||||
|
text = entry.item.name + " " + entry.item.date + " " + entry.text + \
|
||||||
|
" by " + entry.user.name + " " + entry.date
|
||||||
|
writer.add_document(title=entry.item.name, path=path, content=text)
|
||||||
|
writer.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def ft_search_times(query_str, number):
|
||||||
|
"""
|
||||||
|
Search for a given term and returns a specific amount of results.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
query_str (str): term to search for
|
||||||
|
number (int): number of results to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List(Entry): list of entries that matched the search
|
||||||
|
"""
|
||||||
|
ix = open_dir(config.INDEX_DIR)
|
||||||
|
results = []
|
||||||
|
db = Database()
|
||||||
|
with ix.searcher(weighting=scoring.BM25F) as s:
|
||||||
|
query = QueryParser("content", ix.schema).parse(query_str)
|
||||||
|
matches = s.search(query, limit=number)
|
||||||
|
for match in matches:
|
||||||
|
results.append(db.get_entry_by_id(match["path"]))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def ft_search(query_str):
|
||||||
|
"""
|
||||||
|
Search for a given term and show the predefined amount of results.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
query_str (str): term to search for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List(Entry): list of entries that matched the search
|
||||||
|
"""
|
||||||
|
return ft_search_times(query_str, config.SEARCH_NUMBER)
|
@ -27,7 +27,7 @@
|
|||||||
{% set ns.open_li = True -%}
|
{% set ns.open_li = True -%}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
<a href="{{ url_for('entry', ident=entry.id) }}">
|
<a href="{{ url_for('entry', ident=entry.id) }}">
|
||||||
{{ entry.reviewed }} {{ r_to_star(entry.rating) }} by {{ entry.user.name }}
|
{{ entry.date }} {{ r_to_star(entry.rating) }} by {{ entry.user.name }}
|
||||||
</a>
|
</a>
|
||||||
{% set ns.prev_item_date = entry.item.date -%}
|
{% set ns.prev_item_date = entry.item.date -%}
|
||||||
{% set ns.prev_item_id = entry.item.id -%}
|
{% set ns.prev_item_id = entry.item.id -%}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
rated {{ entry.rating }}/100 by
|
rated {{ entry.rating }}/100 by
|
||||||
<a href="{{ url_for('user', name=entry.user.name) }}">
|
<a href="{{ url_for('user', name=entry.user.name) }}">
|
||||||
{{ entry.user.name }}
|
{{ entry.user.name }}
|
||||||
</a> on {{ entry.reviewed }}
|
</a> on {{ entry.date }}
|
||||||
</small><br>
|
</small><br>
|
||||||
{% autoescape off -%}
|
{% autoescape off -%}
|
||||||
{{ entry.text }}
|
{{ entry.text }}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
{{ url_for("index", _anchor=entry.id, _external=True) }}
|
{{ url_for("index", _anchor=entry.id, _external=True) }}
|
||||||
</guid>
|
</guid>
|
||||||
<pubDate>
|
<pubDate>
|
||||||
{{ entry.reviewed }}
|
{{ entry.date }}
|
||||||
</pubDate>
|
</pubDate>
|
||||||
<description>
|
<description>
|
||||||
{% autoescape off -%}
|
{% autoescape off -%}
|
||||||
|
23
src/templates/search.html
Normal file
23
src/templates/search.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{% extends "template.html" -%}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h1>Search</h1><br>
|
||||||
|
<div class="search">
|
||||||
|
<form action="{{ url_for('search') }}" method=post>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ form.query_str }}
|
||||||
|
{{ form.submit }}
|
||||||
|
</form>
|
||||||
|
<ul>
|
||||||
|
{% for entry in results -%}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('entry', ident=entry.id) }}">
|
||||||
|
{{ entry.date }} {{ r_to_star(entry.rating) }} {{ entry.item.name }} ({{ entry.item.date }}) by {{ entry.user.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor -%}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock -%}
|
@ -14,7 +14,7 @@
|
|||||||
</a>
|
</a>
|
||||||
on
|
on
|
||||||
<a href="{{ url_for('index', _anchor=entry.id) }}">
|
<a href="{{ url_for('index', _anchor=entry.id) }}">
|
||||||
{{ entry.reviewed }}
|
{{ entry.date }}
|
||||||
</a>
|
</a>
|
||||||
</small><br>
|
</small><br>
|
||||||
{% if current_user.id == entry.user.id -%}
|
{% if current_user.id == entry.user.id -%}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% set navigation_bar = [
|
{% set navigation_bar = [
|
||||||
(url_for("index"), 'index', 'Blog'),
|
(url_for("index"), "index", "Blog"),
|
||||||
(url_for("archive"), 'archive', 'Archive')
|
(url_for("archive"), "archive", "Archive"),
|
||||||
|
(url_for("search"), "search", "Search")
|
||||||
] -%}
|
] -%}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
{% set ns.open_li = True -%}
|
{% set ns.open_li = True -%}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
<a href="{{ url_for('entry', ident=entry.id) }}">
|
<a href="{{ url_for('entry', ident=entry.id) }}">
|
||||||
{{ entry.reviewed }} {{ r_to_star(entry.rating) }}
|
{{ entry.date }} {{ r_to_star(entry.rating) }}
|
||||||
</a>
|
</a>
|
||||||
{% set ns.prev_item_date = entry.item.date -%}
|
{% set ns.prev_item_date = entry.item.date -%}
|
||||||
{% set ns.prev_item_id = entry.item.id -%}
|
{% set ns.prev_item_id = entry.item.id -%}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user