Add Wiki Pages
This commit is contained in:
parent
2f2ed6169d
commit
0925a9b334
@ -58,11 +58,12 @@ async fn main() -> std::io::Result<()> {
|
|||||||
// USER SERVICES
|
// USER SERVICES
|
||||||
.route("/u/{username}/", web::get().to(user::profile))
|
.route("/u/{username}/", web::get().to(user::profile))
|
||||||
.route("/user/{username}/", web::get().to(user::profile))
|
.route("/user/{username}/", web::get().to(user::profile))
|
||||||
|
// WIKI SERVICES
|
||||||
|
.route("/r/{sub}/wiki/", web::get().to(subreddit::wiki))
|
||||||
|
.route("/r/{sub}/wiki/{page}/", web::get().to(subreddit::wiki))
|
||||||
// SUBREDDIT SERVICES
|
// SUBREDDIT SERVICES
|
||||||
.route("/r/{sub}/", web::get().to(subreddit::page))
|
.route("/r/{sub}/", web::get().to(subreddit::page))
|
||||||
.route("/r/{sub}/{sort}/", web::get().to(subreddit::page))
|
.route("/r/{sub}/{sort:hot|new|top|rising}/", web::get().to(subreddit::page))
|
||||||
// WIKI SERVICES
|
|
||||||
// .route("/r/{sub}/wiki/index", web::get().to(subreddit::wiki))
|
|
||||||
// POPULAR SERVICES
|
// POPULAR SERVICES
|
||||||
.route("/", web::get().to(subreddit::page))
|
.route("/", web::get().to(subreddit::page))
|
||||||
.route("/{sort:best|hot|new|top|rising}/", web::get().to(subreddit::page))
|
.route("/{sort:best|hot|new|top|rising}/", web::get().to(subreddit::page))
|
||||||
|
@ -24,7 +24,7 @@ pub async fn item(req: HttpRequest) -> Result<HttpResponse> {
|
|||||||
// Log the post ID being fetched in debug mode
|
// Log the post ID being fetched in debug mode
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
dbg!(&id);
|
dbg!(&id);
|
||||||
|
|
||||||
// Send a request to the url, receive JSON in response
|
// Send a request to the url, receive JSON in response
|
||||||
match request(&path).await {
|
match request(&path).await {
|
||||||
// Otherwise, grab the JSON output from the request
|
// Otherwise, grab the JSON output from the request
|
||||||
@ -36,9 +36,9 @@ pub async fn item(req: HttpRequest) -> Result<HttpResponse> {
|
|||||||
// Use the Post and Comment structs to generate a website to show users
|
// Use the Post and Comment structs to generate a website to show users
|
||||||
let s = PostTemplate { comments, post, sort }.render().unwrap();
|
let s = PostTemplate { comments, post, sort }.render().unwrap();
|
||||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||||
},
|
}
|
||||||
// If the Reddit API returns an error, exit and send error page to user
|
// If the Reddit API returns an error, exit and send error page to user
|
||||||
Err(msg) => error(msg.to_string()).await
|
Err(msg) => error(msg.to_string()).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ pub async fn find(req: HttpRequest) -> Result<HttpResponse> {
|
|||||||
.render()
|
.render()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||||
},
|
}
|
||||||
Err(msg) => error(msg.to_string()).await
|
Err(msg) => error(msg.to_string()).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,14 @@ struct SubredditTemplate {
|
|||||||
ends: (String, String),
|
ends: (String, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "wiki.html", escape = "none")]
|
||||||
|
struct WikiTemplate {
|
||||||
|
sub: String,
|
||||||
|
wiki: String,
|
||||||
|
}
|
||||||
|
|
||||||
// SERVICES
|
// SERVICES
|
||||||
// web::Path(sub): web::Path<String>, params: web::Query<Params>
|
|
||||||
pub async fn page(req: HttpRequest) -> Result<HttpResponse> {
|
pub async fn page(req: HttpRequest) -> Result<HttpResponse> {
|
||||||
let path = format!("{}.json?{}", req.path(), req.query_string());
|
let path = format!("{}.json?{}", req.path(), req.query_string());
|
||||||
let sub = req.match_info().get("sub").unwrap_or("popular").to_string();
|
let sub = req.match_info().get("sub").unwrap_or("popular").to_string();
|
||||||
@ -37,8 +43,27 @@ pub async fn page(req: HttpRequest) -> Result<HttpResponse> {
|
|||||||
.render()
|
.render()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||||
},
|
}
|
||||||
Err(msg) => error(msg.to_string()).await
|
Err(msg) => error(msg.to_string()).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wiki(req: HttpRequest) -> Result<HttpResponse> {
|
||||||
|
let sub = req.match_info().get("sub").unwrap();
|
||||||
|
let page = req.match_info().get("page").unwrap_or("index");
|
||||||
|
let path: String = format!("r/{}/wiki/{}.json?raw_json=1", sub, page);
|
||||||
|
|
||||||
|
match request(&path).await {
|
||||||
|
Ok(res) => {
|
||||||
|
let s = WikiTemplate {
|
||||||
|
sub: sub.to_string(),
|
||||||
|
wiki: res["data"]["content_html"].as_str().unwrap().to_string(),
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap();
|
||||||
|
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||||
|
}
|
||||||
|
Err(msg) => error(msg.to_string()).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,33 +72,32 @@ async fn subreddit(sub: &str) -> Result<Subreddit, &'static str> {
|
|||||||
// Build the Reddit JSON API url
|
// Build the Reddit JSON API url
|
||||||
let path: String = format!("r/{}/about.json?raw_json=1", sub);
|
let path: String = format!("r/{}/about.json?raw_json=1", sub);
|
||||||
|
|
||||||
let res;
|
|
||||||
|
|
||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match request(&path).await {
|
match request(&path).await {
|
||||||
// If success, receive JSON in response
|
// If success, receive JSON in response
|
||||||
Ok(response) => { res = response; },
|
Ok(res) => {
|
||||||
|
// Metadata regarding the subreddit
|
||||||
|
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
|
||||||
|
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
|
||||||
|
|
||||||
|
// Fetch subreddit icon either from the community_icon or icon_img value
|
||||||
|
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or("").split('?').collect::<Vec<&str>>()[0];
|
||||||
|
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
||||||
|
|
||||||
|
let sub = Subreddit {
|
||||||
|
name: val(&res, "display_name"),
|
||||||
|
title: val(&res, "title"),
|
||||||
|
description: val(&res, "public_description"),
|
||||||
|
info: val(&res, "description_html").replace("\\", ""),
|
||||||
|
icon: format_url(icon).await,
|
||||||
|
members: format_num(members),
|
||||||
|
active: format_num(active),
|
||||||
|
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(sub)
|
||||||
|
}
|
||||||
// If the Reddit API returns an error, exit this function
|
// If the Reddit API returns an error, exit this function
|
||||||
Err(msg) => return Err(msg)
|
Err(msg) => return Err(msg),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata regarding the subreddit
|
|
||||||
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
|
|
||||||
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
|
|
||||||
|
|
||||||
// Fetch subreddit icon either from the community_icon or icon_img value
|
|
||||||
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or("").split('?').collect::<Vec<&str>>()[0];
|
|
||||||
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
|
||||||
|
|
||||||
let sub = Subreddit {
|
|
||||||
name: val(&res, "display_name"),
|
|
||||||
title: val(&res, "title"),
|
|
||||||
description: val(&res, "public_description"),
|
|
||||||
info: val(&res, "description_html").replace("\\", ""),
|
|
||||||
icon: format_url(icon).await,
|
|
||||||
members: format_num(members),
|
|
||||||
active: format_num(active),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(sub)
|
|
||||||
}
|
}
|
||||||
|
12
src/user.rs
12
src/user.rs
@ -25,7 +25,7 @@ pub async fn profile(req: HttpRequest) -> Result<HttpResponse> {
|
|||||||
// Request user profile data and user posts/comments from Reddit
|
// Request user profile data and user posts/comments from Reddit
|
||||||
let user = user(&username).await;
|
let user = user(&username).await;
|
||||||
let posts = fetch_posts(&path, "Comment".to_string()).await;
|
let posts = fetch_posts(&path, "Comment".to_string()).await;
|
||||||
|
|
||||||
match posts {
|
match posts {
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
let s = UserTemplate {
|
let s = UserTemplate {
|
||||||
@ -37,9 +37,9 @@ pub async fn profile(req: HttpRequest) -> Result<HttpResponse> {
|
|||||||
.render()
|
.render()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||||
},
|
}
|
||||||
// If there is an error show error page
|
// If there is an error show error page
|
||||||
Err(msg) => error(msg.to_string()).await
|
Err(msg) => error(msg.to_string()).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,9 +53,11 @@ async fn user(name: &str) -> Result<User, &'static str> {
|
|||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match request(&path).await {
|
match request(&path).await {
|
||||||
// If success, receive JSON in response
|
// If success, receive JSON in response
|
||||||
Ok(response) => { res = response; },
|
Ok(response) => {
|
||||||
|
res = response;
|
||||||
|
}
|
||||||
// If the Reddit API returns an error, exit this function
|
// If the Reddit API returns an error, exit this function
|
||||||
Err(msg) => return Err(msg)
|
Err(msg) => return Err(msg),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab creation date as unix timestamp
|
// Grab creation date as unix timestamp
|
||||||
|
17
src/utils.rs
17
src/utils.rs
@ -4,7 +4,7 @@
|
|||||||
use actix_web::{http::StatusCode, HttpResponse, Result};
|
use actix_web::{http::StatusCode, HttpResponse, Result};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use chrono::{TimeZone, Utc};
|
use chrono::{TimeZone, Utc};
|
||||||
use serde_json::{from_str};
|
use serde_json::from_str;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
// use surf::{client, get, middleware::Redirect};
|
// use surf::{client, get, middleware::Redirect};
|
||||||
|
|
||||||
@ -70,6 +70,7 @@ pub struct Subreddit {
|
|||||||
pub icon: String,
|
pub icon: String,
|
||||||
pub members: String,
|
pub members: String,
|
||||||
pub active: String,
|
pub active: String,
|
||||||
|
pub wiki: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parser for query params, used in sorting (eg. /r/rust/?sort=hot)
|
// Parser for query params, used in sorting (eg. /r/rust/?sort=hot)
|
||||||
@ -130,7 +131,7 @@ pub fn format_num(num: i64) -> String {
|
|||||||
|
|
||||||
// val() function used to parse JSON from Reddit APIs
|
// val() function used to parse JSON from Reddit APIs
|
||||||
pub fn val(j: &serde_json::Value, k: &str) -> String {
|
pub fn val(j: &serde_json::Value, k: &str) -> String {
|
||||||
String::from(j["data"][k].as_str().unwrap_or(""))
|
String::from(j["data"][k].as_str().unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
// nested_val() function used to parse JSON from Reddit APIs
|
// nested_val() function used to parse JSON from Reddit APIs
|
||||||
@ -146,15 +147,17 @@ pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post
|
|||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
match request(&path).await {
|
match request(&path).await {
|
||||||
// If success, receive JSON in response
|
// If success, receive JSON in response
|
||||||
Ok(response) => { res = response; },
|
Ok(response) => {
|
||||||
|
res = response;
|
||||||
|
}
|
||||||
// If the Reddit API returns an error, exit this function
|
// If the Reddit API returns an error, exit this function
|
||||||
Err(msg) => return Err(msg)
|
Err(msg) => return Err(msg),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the list of posts from the JSON response
|
// Fetch the list of posts from the JSON response
|
||||||
match res["data"]["children"].as_array() {
|
match res["data"]["children"].as_array() {
|
||||||
Some(list) => { post_list = list },
|
Some(list) => post_list = list,
|
||||||
None => { return Err("No posts found") }
|
None => return Err("No posts found"),
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut posts: Vec<Post> = Vec::new();
|
let mut posts: Vec<Post> = Vec::new();
|
||||||
@ -250,7 +253,7 @@ pub async fn request(path: &str) -> Result<serde_json::Value, &'static str> {
|
|||||||
Err("Failed to parse page JSON data")
|
Err("Failed to parse page JSON data")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
false => {
|
false => {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
dbg!(format!("{} - Page not found", url));
|
dbg!(format!("{} - Page not found", url));
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
visibility: visible !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
@ -49,7 +48,11 @@ main {
|
|||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#column_one { max-width: 750px; }
|
#column_one {
|
||||||
|
max-width: 750px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -100,10 +103,14 @@ aside {
|
|||||||
height: max-content;
|
height: max-content;
|
||||||
background: var(--outside);
|
background: var(--outside);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user *, #subreddit * { text-align: center; }
|
#user *, #subreddit * { text-align: center; }
|
||||||
|
|
||||||
|
#subreddit { padding: 0; }
|
||||||
|
#sub_meta { padding: 20px; }
|
||||||
|
|
||||||
#sidebar, #sidebar_contents { margin-top: 20px; }
|
#sidebar, #sidebar_contents { margin-top: 20px; }
|
||||||
|
|
||||||
#sidebar_label {
|
#sidebar_label {
|
||||||
@ -111,7 +118,7 @@ aside {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user_icon, #subreddit_icon {
|
#user_icon, #sub_icon {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border: 2px solid var(--accent);
|
border: 2px solid var(--accent);
|
||||||
@ -120,30 +127,55 @@ aside {
|
|||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user_title, #subreddit_title {
|
#user_title, #sub_title {
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user_description, #subreddit_description {
|
#user_description, #sub_description {
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user_name, #subreddit_name, #user_icon, #subreddit_icon, #user_description, #subreddit_description { margin-bottom: 20px; }
|
#user_name, #sub_name, #user_icon, #sub_icon, #user_description, #sub_description { margin-bottom: 20px; }
|
||||||
|
|
||||||
#user_details, #subreddit_details {
|
#user_details, #sub_details {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
grid-column-gap: 20px;
|
grid-column-gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user_details > label, #subreddit_details > label {
|
#user_details > label, #sub_details > label {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Wiki Pages */
|
||||||
|
|
||||||
|
#wiki {
|
||||||
|
background: var(--foreground);
|
||||||
|
padding: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top {
|
||||||
|
background: var(--highlighted);
|
||||||
|
font-size: 18px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: center;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top > div {
|
||||||
|
border-bottom: 2px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
/* Sorting and Search */
|
/* Sorting and Search */
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -500,11 +532,11 @@ input[type="submit"]:hover { color: var(--accent); }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md a {
|
.md a {
|
||||||
text-decoration: underline;
|
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.md li { margin: 10px 0; }
|
.md li { margin: 10px 0; }
|
||||||
|
.toc_child { list-style: none; }
|
||||||
|
|
||||||
.md pre {
|
.md pre {
|
||||||
background: var(--outside);
|
background: var(--outside);
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<link rel="stylesheet" href="/style.css">
|
<link rel="stylesheet" href="/style.css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body style="visibility: hidden;">
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
<a id="logo" href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a>
|
<a id="logo" href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a>
|
||||||
{% block search %}{% endblock %}
|
{% block search %}{% endblock %}
|
||||||
|
@ -66,15 +66,23 @@
|
|||||||
{% if sub.name != "" %}
|
{% if sub.name != "" %}
|
||||||
<aside>
|
<aside>
|
||||||
<div id="subreddit">
|
<div id="subreddit">
|
||||||
<img id="subreddit_icon" src="{{ sub.icon }}">
|
{% if sub.wiki %}
|
||||||
<p id="subreddit_title">{{ sub.title }}</p>
|
<div id="top">
|
||||||
<p id="subreddit_name">r/{{ sub.name }}</p>
|
<div>Posts</div>
|
||||||
<p id="subreddit_description">{{ sub.description }}</p>
|
<a href="/r/{{ sub.name }}/wiki/index">Wiki</a>
|
||||||
<div id="subreddit_details">
|
</div>
|
||||||
<label>Members</label>
|
{% endif %}
|
||||||
<label>Active</label>
|
<div id="sub_meta">
|
||||||
<div>{{ sub.members }}</div>
|
<img id="sub_icon" src="{{ sub.icon }}">
|
||||||
<div>{{ sub.active }}</div>
|
<p id="sub_title">{{ sub.title }}</p>
|
||||||
|
<p id="sub_name">r/{{ sub.name }}</p>
|
||||||
|
<p id="sub_description">{{ sub.description }}</p>
|
||||||
|
<div id="sub_details">
|
||||||
|
<label>Members</label>
|
||||||
|
<label>Active</label>
|
||||||
|
<div>{{ sub.members }}</div>
|
||||||
|
<div>{{ sub.active }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<details id="sidebar">
|
<details id="sidebar">
|
||||||
|
25
templates/wiki.html
Normal file
25
templates/wiki.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import "utils.html" as utils %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% if sub != "" %}{{ sub }}
|
||||||
|
{% else %}Libreddit{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block search %}
|
||||||
|
{% call utils::search(["/r/", sub.as_str()].concat(), "") %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<main>
|
||||||
|
<div id="column_one">
|
||||||
|
<div id="top">
|
||||||
|
<a href="/r/{{ sub }}">Posts</a>
|
||||||
|
<div>Wiki</div>
|
||||||
|
</div>
|
||||||
|
<div id="wiki">
|
||||||
|
{{ wiki }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user