added search page, updated style

master
tiyn 8 months ago
parent a936fd5ee6
commit 8796169ff7

@ -13,6 +13,9 @@ via plain text files.
- [x] HTML files (.html)
- [x] Markdown Files (.md)
- [x] Infinite-scroll blog page
- [x] Search page
- [x] Full-text search
- [x] Preview panel
- [x] Archive page
- [x] Months as headings
- [x] Links to scrolling blog page

@ -1,10 +1,9 @@
from flask import Flask, make_response, render_template, abort
from flask import Flask, abort, make_response, render_template, request
from flask_font_awesome import FontAwesome
import content as con_gen
import config
app = Flask(__name__)
import content as con_gen
from forms import SearchForm, register_csrf
TITLE = config.TITLE
STITLE = config.STITLE
@ -14,34 +13,89 @@ DESCRIPTION = config.DESCRIPTION
WEBSITE = config.WEBSITE
MAIL = config.MAIL
app = Flask(__name__)
register_csrf(app)
font_awesome = FontAwesome(app)
@app.errorhandler(404)
def page_not_found(e):
return render_template("error.html", title=TITLE, stitle=STITLE, errorcode="404", style=STYLE, language=LANGUAGE), 404
return render_template("error.html",
title=TITLE,
stitle=STITLE,
errorcode="404",
style=STYLE,
language=LANGUAGE), 404
@app.route("/")
@app.route("/index.html")
def index():
content = con_gen.gen_index_string()
return render_template("index.html", title=TITLE, stitle=STITLE, content_string=content, style=STYLE, language=LANGUAGE)
return render_template("index.html",
title=TITLE,
stitle=STITLE,
content_string=content,
style=STYLE,
language=LANGUAGE)
@app.route("/search", methods=["GET", "POST"])
@app.route("/search.html", methods=["GET", "POST"])
def search():
form = SearchForm()
if request.method == "POST":
query_str = request.form["query_str"]
content = con_gen.gen_query_res_string(query_str)
return render_template("search.html",
title=TITLE,
stitle=STITLE,
style=STYLE,
form=form,
content=content,
language=LANGUAGE), 200
return render_template("search.html",
title=TITLE,
stitle=STITLE,
style=STYLE,
form=form,
content="",
language=LANGUAGE), 200
@app.route("/imprint")
@app.route("/imprint.html")
def imprint():
return render_template("imprint.html", title=TITLE, stitle=STITLE, mail=MAIL, style=STYLE, language=LANGUAGE)
return render_template("imprint.html",
title=TITLE,
stitle=STITLE,
mail=MAIL,
style=STYLE,
language=LANGUAGE)
@app.route("/archive")
@app.route("/archive.html")
def archive():
content = con_gen.gen_arch_string()
return render_template("archive.html", title=TITLE, stitle=STITLE, content_string=content, style=STYLE, language=LANGUAGE)
return render_template("archive.html",
title=TITLE,
stitle=STITLE,
content_string=content,
style=STYLE,
language=LANGUAGE)
@app.route("/entry/<path>")
def entry(path):
content = con_gen.gen_stand_string(path)
if content != "":
return render_template("standalone.html", title=TITLE, stitle=STITLE, content_string=content, style=STYLE, language=LANGUAGE)
return render_template("standalone.html",
title=TITLE,
stitle=STITLE,
content_string=content,
style=STYLE,
language=LANGUAGE)
abort(404)
@ -49,8 +103,11 @@ def entry(path):
@app.route("/rss.xml")
def feed():
content = con_gen.get_rss_string()
rss_xml = render_template("rss.xml", content_string=content, title=TITLE,
description=DESCRIPTION, website=WEBSITE)
rss_xml = render_template("rss.xml",
content_string=content,
title=TITLE,
description=DESCRIPTION,
website=WEBSITE)
response = make_response(rss_xml)
response.headers["Content-Type"] = "application/rss+xml"
return response

@ -18,3 +18,6 @@ LANGUAGE = "en-us"
# Mail address for the imprint
MAIL = "dummy@mail.com"
# Directory to store entries in
ENTRY_DIR = "templates/entry"

@ -4,17 +4,17 @@ import pathlib
from datetime import datetime
from os import path
import config
import markdown
ENTRY_DIR = "templates/entry"
import config
import search
ENTRY_DIR = config.ENTRY_DIR
LANGUAGE = config.LANGUAGE
LOCAL = "de_DE.UTF-8" if LANGUAGE == "de-de" else "en_US.UTF-8"
locale.setlocale(locale.LC_TIME, LOCAL)
standalone_str = "Artikel" if LANGUAGE == "de-de" else "standalone"
def gen_arch_string():
"""
@ -70,7 +70,7 @@ def gen_index_string():
contents = sorted(full_list, key=os.path.getmtime)
for file in reversed(contents):
filename = pathlib.PurePath(file)
purefile = filename
# purefile = filename
title = open(filename).readline().rstrip("\n")
text = open(filename).readlines()[1:]
filename = filename.name
@ -161,11 +161,11 @@ def get_rss_string():
string: rss-string of everything that is in the ENTRY_DIR.
"""
path_ex = ENTRY_DIR
content_string = ""
if path.exists(path_ex):
name_list = os.listdir(path_ex)
full_list = [os.path.join(path_ex, i) for i in name_list]
contents = sorted(full_list, key=os.path.getmtime)
content_string = ""
for file in reversed(contents):
filename = pathlib.PurePath(file)
title = open(filename).readline().rstrip("\n")
@ -186,3 +186,66 @@ def get_rss_string():
content_string += "</description>\n"
content_string += "</item>\n"
return content_string
def gen_query_res_string(query_str):
"""
Return the results of a query.
Parameters:
query_str (string): term to search
Returns:
string: html-formated search result
"""
src_results = search.search(query_str)
res_string = ""
for result in src_results:
title = result["title"]
path = result["path"]
filename = pathlib.PurePath(path)
filename = filename.name
if filename[0] != ".":
filename = filename.split(".", 1)[0]
curr_date = datetime.fromtimestamp(os.path.getmtime(path)).strftime("%Y-%m-%d")
is_markdown = path.endswith(".md")
preview = create_preview(path, is_markdown)
path = "/entry/" + path.split("/", 2)[2]
res_string += "<div class=\"entry\">"
res_string += "<a href=\"" + path + "\"><h2>" + title + "</h2></a>"
res_string += "<small>"
res_string += "<a href=\"" + "/index.html#" + \
filename + "\">" + curr_date + "</a>"
res_string += "</small><br><br>"
res_string += preview + "</div>"
return res_string
def create_preview(path, is_markdown):
"""
Create a preview of a given article and return it.
Parameters:
path (string): path to the article
Returns:
string: html-formated preview
"""
file = open(path, "r", encoding="utf-8")
first_lines = file.readlines()
preview = ""
preview_length = 3
for i, line in enumerate(first_lines):
if i == 0:
continue
if i > preview_length:
break
if not line.isspace():
if is_markdown:
preview += markdown.markdown(line)
else:
preview += line
else:
preview_length += 1
preview += "<br>...<br>"
return preview

@ -0,0 +1,16 @@
import os
from flask_wtf import CSRFProtect, FlaskForm
from wtforms import StringField, SubmitField, ValidationError, validators
def register_csrf(app):
csrf = CSRFProtect()
SECRET_KEY = os.urandom(32)
app.secret_key = SECRET_KEY
csrf.init_app(app)
class SearchForm(FlaskForm):
query_str = StringField("Query", [validators.DataRequired("Please enter the search term")])
# submit = SubmitField("Search")

@ -1,2 +1,7 @@
Flask
Markdown
Whoosh
WTForms
Flask_WTF
MarkupSafe
Font-Awesome-Flask

@ -0,0 +1,72 @@
import os
from whoosh import scoring
from whoosh.fields import ID, TEXT, Schema
from whoosh.index import create_in, open_dir
from whoosh.qparser import QueryParser
import config
INDEX_DIR = "indexdir"
DEF_TOPN = 10
ENTRY_DIR = config.ENTRY_DIR
def createSearchableData(root):
"""
Schema definition: title(name of file), path(as ID), content(indexed but not stored), textdata (stored text content)
source:
https://appliedmachinelearning.blog/2018/07/31/developing-a-fast-indexing-and-full-text-search-engine-with-whoosh-a-pure-pythhon-library/
"""
schema = Schema(title=TEXT(stored=True), path=ID(stored=True), content=TEXT)
if not os.path.exists(INDEX_DIR):
os.mkdir(INDEX_DIR)
ix = create_in(INDEX_DIR, schema)
writer = ix.writer()
for r, _, f in os.walk(root):
for file in f:
path = os.path.join(r, file)
fp = open(path, encoding="utf-8")
title = fp.readline()
text = title + fp.read()
writer.add_document(title=title, path=path, content=text)
fp.close()
writer.commit()
def search_times(query_str, topN):
"""
Search for a given term and returns a specific amount of results.
Parameters:
query_str (string): term to search for
topN (int): number of results to return
Returns:
string: html-formatted string including the hits of the search
"""
ix = open_dir(INDEX_DIR)
results = []
with ix.searcher(weighting=scoring.BM25F) as s:
query = QueryParser("content", ix.schema).parse(query_str)
matches = s.search(query, limit=topN)
for match in matches:
results.append({"title": match["title"], "path": match["path"], "match": match.score})
return results
def search(query_str):
"""
Search for a given term and show the predefined amount of results.
Parameters:
query_str (string): term to search for
Returns:
string: html-formatted string including the hits of the search
"""
return search_times(query_str, DEF_TOPN)
createSearchableData(ENTRY_DIR)

@ -3,6 +3,7 @@
:root {
--bg0: rgb(29,32,33);
--color0: rgb(220,120,0);
--color1: rgb(280,180,0);
--error: rgb(255,0,0);
--footerbg0: rgb(29,32,33);
--link0: rgb(220, 120, 0);
@ -83,3 +84,13 @@ span {
.entry h2 {
color: var(--text1);
}
form.search button[type=submit] {
background: var(--color0);
color: var(--bg0);
}
form.search button[type=submit]:hover {
background: var(--color1);
}

@ -3,6 +3,7 @@
:root {
--bg0: rgb(255,255,255);
--color0: rgb(0,0,120);
--color1: rgb(0,0,200);
--error: rgb(255,0,0);
--footerbg0: rgb(192,192,192);
--link0: rgb(0,0,120);
@ -91,3 +92,12 @@ span {
.entry h2 {
color: var(--text1);
}
form.search button[type=submit] {
background: var(--color0);
color: var(--bg0);
}
form.search button[type=submit]:hover {
background: var(--color1);
}

@ -157,6 +157,10 @@ footer .center {
}
}
form {
margin-bottom: 40px;
}
.entry {
border-radius: 0 10px 30px 0;
margin-bottom: 20px;
@ -186,6 +190,15 @@ h3 {
padding-bottom: 0;
}
.blogarchive h1:first-child {
padding-bottom: 0;
}
.blogarchive h2:first-child {
padding-top: 0;
padding-bottom: 0;
}
.entry ul {
padding-left: 20;
}
@ -216,3 +229,28 @@ code {
white-space: pre;
display: inline-block
}
form.search input[type=text] {
padding: 10px;
font-size: 17px;
border: 1px solid grey;
float: left;
width: 50%;
background: #f1f1f1;
}
form.search button[type=submit] {
float: left;
width: 5%;
padding: 12px;
font-size: 17px;
border: 1px solid grey;
border-left: none; /* Prevent double borders */
cursor: pointer;
}
form.search::after {
content: "";
clear: both;
display: table;
}

@ -0,0 +1,19 @@
{% extends "template.html" %}
{% block content %}
<div class="container">
<div class="search">
<h1>{% if language=="de-de" %}Suche{% else %}Search{% endif %}</h1><br>
<form class="search" action="{{ url_for('search') }}" method=post>
{{ form.hidden_tag() }}
{{ form.query_str }}
<!-- {{ form.submit }} -->
<button id="submit" name="submit" type="submit" value="Search">{{ font_awesome.render_icon("fas fa-search")
}}</button>
</form>
{% autoescape off %}
{{ content }}
{% endautoescape %}
</div>
</div>
{% endblock %}

@ -1,11 +1,14 @@
<html>
<head>
<title>{{ title }}</title>
<link href="{{ url_for('static', filename='css/' + style + '.css') }}" rel="stylesheet" type="text/css">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='graphics/logo.png') }}">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width" initial-scale=1.0>
{{ font_awesome.load_js() }}
</head>
<body>
<!-- Menu -->
<div class="main-menu-dropdown">
@ -18,6 +21,7 @@
<div class="main-menu">
<a href="{{ url_for('index') }}">Blog</a>
<a href="{{ url_for('archive') }}">{% if language=="de-de" %}Archiv{% else %}Archive{% endif %}</a>
<a href="{{ url_for('search') }}">{% if language=="de-de" %}Suche{% else %}Search{% endif %}</a>
<label for="main-menu-check" class="hide-menu">X</label>
</div>
</div>
@ -35,4 +39,5 @@
</div>
</footer>
</body>
</html>

Loading…
Cancel
Save