feat: Add basic frontend
This commit is contained in:
parent
b5bb18add6
commit
ca79aed027
14 changed files with 7620 additions and 15 deletions
0
fegen/__init__.py
Normal file
0
fegen/__init__.py
Normal file
111
fegen/discourse.py
Normal file
111
fegen/discourse.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
'''
|
||||
Class to generate a csv file based on data fetched via Discourse REST API
|
||||
'''
|
||||
import os
|
||||
import csv
|
||||
import json
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
DISCOURSE_URL = "https://kb.hs3.pl" # Database is hosted here
|
||||
CATEGORY_ID = 9 # Database category ID
|
||||
|
||||
class DiscourseDatabase():
|
||||
def __init__(self):
|
||||
data = self.get_category_data()
|
||||
self.category_topics_csv(data)
|
||||
load_dotenv()
|
||||
|
||||
def get_headers(self, auth=False):
|
||||
"""Get request headers, optionally with auth data."""
|
||||
headers = {
|
||||
"content-type": "application/json",
|
||||
}
|
||||
if auth:
|
||||
headers["Api-Key"] = os.getenv("DISCOURSE_PAT")
|
||||
headers["Api-Username"] = os.getenv("DISCOURSE_USERNAME")
|
||||
return headers
|
||||
|
||||
def get_category_data(self) -> dict:
|
||||
"""Get all topics from a Discourse category with pagination"""
|
||||
url = f"{DISCOURSE_URL}/c/{CATEGORY_ID}.json"
|
||||
print(f"Fetching data from {url}")
|
||||
all_topics = []
|
||||
page = 0
|
||||
while True:
|
||||
params = {"per_page": 100, "page": page}
|
||||
res = requests.get(url, headers=self.get_headers(), params=params)
|
||||
res.raise_for_status()
|
||||
res_json = res.json()
|
||||
topics = res_json["topic_list"]["topics"]
|
||||
if not topics:
|
||||
break
|
||||
for topic in topics:
|
||||
if topic["category_id"] == CATEGORY_ID:
|
||||
all_topics.append(topic)
|
||||
print(f"Fetched page {page}: {len(topics)} topics, {len(all_topics)} total in category")
|
||||
page += 1
|
||||
return {"topic_list": {"topics": all_topics}}
|
||||
|
||||
def get_topic_content(self, topic_id: str):
|
||||
"""Get a single topic's content"""
|
||||
get_url = f"{DISCOURSE_URL}/posts/{topic_id}.json"
|
||||
res = requests.get(get_url, headers=self.get_headers(auth=True))
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
|
||||
def category_topics_csv(self, category_data) -> None:
|
||||
"""Save category topics to a csv file"""
|
||||
columns = ["id", "title", "place", "tags"]
|
||||
records = category_data["topic_list"]["topics"]
|
||||
with open('zasoby.csv', 'w', encoding='UTF8') as f:
|
||||
write = csv.writer(f)
|
||||
write.writerow(columns)
|
||||
for topic in records:
|
||||
html_url = f'<a href="{DISCOURSE_URL}/t/{topic["id"]}">{topic["title"]}</a>'
|
||||
place = self.get_place(topic)
|
||||
write.writerow([topic["id"], html_url, place, topic["tags"]])
|
||||
print(f"New zasoby.csv generated with {len(records)} records")
|
||||
|
||||
def get_place(self, topic):
|
||||
"""Get place of a topic"""
|
||||
places = ["cow-work", "garage", "lab"]
|
||||
for place in places:
|
||||
if place in topic["tags"]:
|
||||
return f'<a href="https://kb.hs3.pl/tag/{place}">{place}</a>'
|
||||
return "unknown"
|
||||
|
||||
def replace_string_in_post(self, topic_id: str, old_string: str, new_string: str) -> dict:
|
||||
"""Replace a selected string within a topic's first post using Discourse REST API"""
|
||||
# Fetch the topic to get the first post ID
|
||||
topic_url = f"{DISCOURSE_URL}/t/{topic_id}.json"
|
||||
topic_res = requests.get(topic_url, headers=self.get_headers(auth=True))
|
||||
topic_res.raise_for_status()
|
||||
topic_data = topic_res.json()
|
||||
|
||||
# Get the first post ID from the topic
|
||||
first_post_id = topic_data["post_stream"]["posts"][0]["id"]
|
||||
|
||||
# Fetch the post content
|
||||
post_url = f"{DISCOURSE_URL}/posts/{first_post_id}.json"
|
||||
post_res = requests.get(post_url, headers=self.get_headers(auth=True))
|
||||
post_res.raise_for_status()
|
||||
post_data = post_res.json()
|
||||
|
||||
# Replace the string
|
||||
updated_raw = post_data["raw"].replace(old_string, new_string)
|
||||
|
||||
# Update the post
|
||||
payload = {"post": {"raw": updated_raw}}
|
||||
res = requests.put(post_url, json=payload, headers=self.get_headers(auth=True))
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
|
||||
if __name__ == "__main__":
|
||||
disc = DiscourseDatabase()
|
||||
category = disc.get_category_data()
|
||||
records = category["topic_list"]["topics"]
|
||||
for topic in records:
|
||||
if "lab" in topic["tags"]:
|
||||
disc.replace_string_in_post(topic["id"], "[Workshop](https://kb.s.hs3.pl/tag/workshop)", "[Lab](https://kb.s.hs3.pl/tag/lab)")
|
||||
|
||||
7030
fegen/docs/index.html
Normal file
7030
fegen/docs/index.html
Normal file
File diff suppressed because one or more lines are too long
14
fegen/docs/static/css/style.css
vendored
Normal file
14
fegen/docs/static/css/style.css
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
footer {
|
||||
padding: 12px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* || Sidenav */
|
||||
.sidenav {
|
||||
width: 250px;
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
overflow-x: hidden;
|
||||
padding: 6px 8px 6px 16px;
|
||||
}
|
||||
44
fegen/main.py
Normal file
44
fegen/main.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import os, re, shutil
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def add_download_button(row):
|
||||
item_id = re.sub(r'<a href.*/([0-9]+)".*', "\\1", row[1])
|
||||
download_button = (
|
||||
f'<button id="btn_{item_id}"><i class="fa fa-download"></i> {item_id}</button>'
|
||||
)
|
||||
print_button = f'<a href="http://localhost:31337/print/{item_id}"><button id="prn_{item_id}"><i class="fa fa-print"></i> {item_id}</button></a>'
|
||||
return row + [download_button, print_button]
|
||||
|
||||
|
||||
def generate_dashboard():
|
||||
"""Generate dashboard from zasoby.csv file"""
|
||||
print("Generating HTML dashboard")
|
||||
website_folder = "fegen/docs"
|
||||
data = pd.read_csv("zasoby.csv")
|
||||
env = Environment(loader=FileSystemLoader("fegen/template"))
|
||||
print("Removing old website files")
|
||||
print("Creating a new website")
|
||||
shutil.copytree("fegen/template/static", f"{website_folder}/static", dirs_exist_ok=True)
|
||||
template = env.get_template("_main_layout.html")
|
||||
with open(f"{website_folder}/index.html", "w+", encoding="utf-8") as file:
|
||||
header_row = data.columns.values.tolist() + ["label", "print"]
|
||||
rows = map(
|
||||
add_download_button,
|
||||
data.values.tolist(),
|
||||
)
|
||||
html = template.render(
|
||||
title="Baza Zasobów Hackerspace Trójmiasto",
|
||||
t_header=header_row,
|
||||
t_body=rows,
|
||||
)
|
||||
file.write(html)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from discourse import DiscourseDatabase
|
||||
|
||||
DiscourseDatabase()
|
||||
generate_dashboard()
|
||||
print("Done!")
|
||||
150
fegen/template/_base_template.html
Normal file
150
fegen/template/_base_template.html
Normal file
File diff suppressed because one or more lines are too long
29
fegen/template/_main_layout.html
Normal file
29
fegen/template/_main_layout.html
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "_base_template.html" %} {% block body %}
|
||||
<div id="site-body" class="container-fluid">
|
||||
<div class="row flex-nonwrap">
|
||||
<div class="sidenav">{% block sidenav %}{% endblock sidenav %}</div>
|
||||
<div class="main">
|
||||
<h1>Baza Zasobów Hackerspace Trójmiasto</h1>
|
||||
<a href="/refresh"><button id="refresh"><i class="fa fa-arrows-rotate"></i> Refresh</button></a>
|
||||
<table id="dashboardTable">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for cell in t_header %}
|
||||
<td>{{cell}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in t_body %}
|
||||
<tr>
|
||||
{% for cell in row %}
|
||||
<td>{{cell}}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock body %}
|
||||
14
fegen/template/static/css/style.css
Normal file
14
fegen/template/static/css/style.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
footer {
|
||||
padding: 12px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* || Sidenav */
|
||||
.sidenav {
|
||||
width: 250px;
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
overflow-x: hidden;
|
||||
padding: 6px 8px 6px 16px;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue