42 Commits

Author SHA1 Message Date
a0853ed3de static template context for errors
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-20 13:44:17 +03:00
cae6b0c97f add json routes 2023-08-20 13:17:52 +03:00
9c3a016c82 disable index on static/ file server 2023-08-20 12:11:09 +03:00
28c01a7fc6 add 500 catcher 2023-08-20 12:10:11 +03:00
05073604a9 improve 404 catcher 2023-08-20 11:52:39 +03:00
b394c7e0f8 update 404 template 2023-08-20 11:51:50 +03:00
d63bc7a758 fix rating field in answer template
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-18 22:26:30 +03:00
da962a9f7a v0.2.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-18 11:14:36 +03:00
Dmitry Belyaev
f86a3a9050 Merge pull request 'migrate/rocket/0.5' (#3) from migrate/rocket/0.5 into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3
2023-08-18 08:07:13 +00:00
971c79a111 use async db
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-18 10:50:56 +03:00
a4dd460c37 use serde from rocket_dyn_templates
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-05 19:42:19 +03:00
67087e6583 ci: add pipeline type
Some checks failed
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is passing
2023-08-05 19:31:35 +03:00
ca75f84ee2 ci: fix img
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-05 19:25:50 +03:00
a951eff714 ci: use rust stable
Some checks failed
continuous-integration/drone/push Build encountered an error
2023-08-05 19:24:47 +03:00
18eb3ec51b min migrate to rocket 0.5
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-05 19:21:00 +03:00
78d57ca2e6 update lockfile
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-05 17:50:49 +03:00
103391c891 mini_moka
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-04 11:39:52 +03:00
6d1d8eaea1 upd deps
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-03 15:43:30 +03:00
d1cb2187c1 add cache
All checks were successful
continuous-integration/drone/push Build is passing
b4tman/qchgk_web#1
using moka
2023-04-02 17:01:51 +03:00
d9a060b1b7 add .drone.yml
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-03-29 09:34:01 +03:00
d6571959db Merge branch 'rocket' 2023-03-28 16:37:03 +03:00
b281f85a70 share database reader instance 2023-03-28 16:36:04 +03:00
89735f98d3 upd chgk_ledb 2023-03-28 16:30:40 +03:00
f56ccf471f new db 2023-01-04 01:12:03 +03:00
747a611a67 use chgk_ledb_lib 2022-11-11 20:56:35 +03:00
1eadfac11a Merge branch 'rocket' 2022-10-04 00:20:27 +03:00
1b833cff90 add README 2022-10-04 00:17:02 +03:00
3097ef9f08 add LICENSE 2022-10-04 00:11:17 +03:00
0f837db0a2 refactor show_question_details 2022-10-04 00:07:25 +03:00
b3fcc66b1b upd deps 2022-10-04 00:06:08 +03:00
8231274658 back to rand 0.7 2021-02-06 17:38:25 +03:00
ee2e8cd560 upd + show answer button 2021-02-06 00:46:53 +03:00
eb6349b51b upd ledb to 0.4 2020-08-18 21:51:46 +03:00
1c2e08663c deps update 2020-08-12 09:15:14 +03:00
39fdf46037 redesign with shadows 2019-08-22 15:18:38 +03:00
987593a48d upd bootstrap to 4.3.1 2019-08-22 14:19:14 +03:00
40f9a399dc shrink-to-fit=no 2019-08-22 14:16:43 +03:00
afe12bd630 base template: add more space to bottom 2019-08-22 13:48:37 +03:00
5c49405ac0 cargo update 2019-08-22 13:42:48 +03:00
665bbaadfb + viewport meta
(cherry picked from commit aec2e87472)
2019-08-19 22:05:42 +03:00
fb40a5a862 redesign 2019-08-19 15:58:47 +03:00
2a6cd616be from actix to rocket 2019-08-05 17:06:27 +03:00
19 changed files with 2278 additions and 1892 deletions

13
.drone.yml Normal file
View File

@@ -0,0 +1,13 @@
kind: pipeline
type: docker
name: default
steps:
- name: build
image: rust:1-alpine
commands:
- apk add --no-cache musl-dev
- cargo fetch
- cargo build --all
environment:
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse

4
.gitignore vendored
View File

@@ -1,4 +1,4 @@
/target /target
**/*.rs.bk **/*.rs.bk
db.dat
/db /db

3478
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,25 @@
[package] [package]
name = "qchgk_web" name = "qchgk_web"
version = "0.1.0" version = "0.2.0"
authors = ["Dmitry <b4tm4n@mail.ru>"] authors = ["Dmitry <b4tm4n@mail.ru>"]
edition = "2018" edition = "2021"
license = "MIT"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
actix-files="0.1" rand="0.8"
actix-web = "1.0" rocket = { version = "=0.5.0-rc.3", features = ["json"] }
serde="1.0" rocket_dyn_templates = { version = "=0.1.0-rc.3", features = ["tera"] }
serde_derive="1.0" chgk_ledb_lib = {git = "https://gitea.b4tman.ru/b4tman/chgk_ledb.git", rev="699478f85e", package="chgk_ledb_lib", features=["async"]}
serde_json="1.0" mini-moka = "0.10.0"
ledb="0.2" lazy_static = "1.4.0"
ledb-derive="0.2"
ledb-types="0.2" [profile.release]
lmdb-zero="0.4" opt-level = 3
rand="0.7" debug = false
env_logger = "0.6" lto = true
tera = "0.11" strip = true
# actix="0.7"
# tokio="0.1"
# futures="0.1"

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Dmitry Belyaev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

10
README.md Normal file
View File

@@ -0,0 +1,10 @@
# qchgk_web
Простой веб интерфейс к базе вопросов ЧГК
## Ссылки
- Источник вопросов: http://db.chgk.info
- Копия файлов базы вопросов: https://gitlab.com/b4tman/db_chgk
- Конвертор в JSON: https://gitea.b4tman.ru/b4tman/chgk_txt2json
- Загрузчик в формат базы для этой штуки: https://gitea.b4tman.ru/b4tman/chgk_ledb

View File

@@ -1,235 +1,253 @@
// extern crate actix;
extern crate actix_files;
extern crate actix_web;
extern crate serde;
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate rocket;
#[macro_use]
extern crate serde_json;
#[macro_use]
extern crate ledb;
#[macro_use]
extern crate ledb_derive;
extern crate env_logger;
extern crate ledb_types;
#[macro_use]
extern crate tera;
use tera::Context; use rocket::fs::FileServer;
use rocket::fs::Options;
use rocket::http::ContentType;
use rocket::request::Request;
use rocket::response::Redirect;
use rocket::response::{self, Responder, Response};
use rocket::State;
use rocket_dyn_templates::tera;
use rocket_dyn_templates::Template;
// extern crate futures; use rand::distributions::Uniform;
use rand::Rng;
// extern crate tokio; use lazy_static::lazy_static;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::Arc;
use actix_web::{ use chgk_ledb_lib::async_db;
error, guard, http::header, http::Method, middleware::Logger, middleware::NormalizePath, web, use chgk_ledb_lib::questions::Question;
App, Error, HttpRequest, HttpResponse, HttpServer, Responder, Result,
};
use std::cell::Cell;
use rand::seq::IteratorRandom; use mini_moka::sync::Cache;
use std::time::Instant; use std::time::Duration;
use std::{fs, io}; const DB_FILENAME: &str = "db.dat";
// use tokio::spawn;
// use futures::{Future};
// use actix::Actor; lazy_static! {
// use actix::System; static ref EMPTY_MAP: HashMap<String, String> = HashMap::new();
//use crate::tokio::prelude::Future;
//use ledb_actix::{Document, Options, Storage, StorageAddrExt};
use ledb::{Options, Storage};
#[derive(Debug, Default, Clone, Serialize, Deserialize, Document)]
struct BatchInfo {
#[document(primary)]
#[serde(default)]
filename: String,
#[serde(default)]
description: String,
#[serde(default)]
author: String,
#[serde(default)]
comment: String,
#[serde(default)]
url: String,
#[serde(default)]
date: String,
#[serde(default)]
processed_by: String,
#[serde(default)]
redacted_by: String,
#[serde(default)]
copyright: String,
#[serde(default)]
theme: String,
#[serde(default)]
kind: String,
#[serde(default)]
source: String,
#[serde(default)]
rating: String,
} }
#[derive(Debug, Default, Clone, Serialize, Deserialize, Document)] trait ErrorEmpty {
struct Question { type Output;
#[document(primary)] fn err_empty(self) -> Result<Self::Output, ()>;
#[serde(default)]
num: u32,
#[document(index)]
id: String,
description: String,
answer: String,
#[serde(default)]
author: String,
#[serde(default)]
comment: String,
#[serde(default)]
comment1: String,
#[serde(default)]
tour: String,
#[serde(default)]
url: String,
#[serde(default)]
date: String,
#[serde(default)]
processed_by: String,
#[serde(default)]
redacted_by: String,
#[serde(default)]
copyright: String,
#[serde(default)]
theme: String,
#[serde(default)]
kind: String,
#[serde(default)]
source: String,
#[serde(default)]
rating: String,
#[document(nested)]
#[serde(default)]
batch_info: BatchInfo,
} }
impl<T, E> ErrorEmpty for Result<T, E> {
type Output = T;
fn err_empty(self) -> Result<Self::Output, ()> {
self.map_err(|_| ())
}
}
#[derive(Debug, Responder)]
enum WebError {
#[response(status = 404)]
NotFound(Template),
#[response(status = 500)]
ServerError(Template),
}
impl WebError {
fn not_found() -> Self {
WebError::NotFound(Template::render("404", EMPTY_MAP.deref()))
}
fn server_error() -> Self {
WebError::ServerError(Template::render("500", EMPTY_MAP.deref()))
}
}
/// wrapper for terra:Value (json, context), to cache values by ref
/// implements Responder as json content type
#[derive(Clone)]
struct ArcTemplateData {
value: Arc<tera::Value>,
}
impl Deref for ArcTemplateData {
type Target = tera::Value;
fn deref(&self) -> &Self::Target {
self.value.deref()
}
}
impl ArcTemplateData {
fn new(value: tera::Value) -> ArcTemplateData {
ArcTemplateData {
value: Arc::new(value),
}
}
fn render(&self, name: &'static str) -> Template {
Template::render(name, self.deref())
}
}
impl<'r> Responder<'r, 'static> for ArcTemplateData {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
Response::build_from(self.to_string().respond_to(req)?)
.header(ContentType::JSON)
.ok()
}
}
type TemplateCache = mini_moka::sync::Cache<usize, ArcTemplateData>;
type DataBaseInner = async_db::Reader<Question>;
type DataBase = Arc<DataBaseInner>;
struct AppState { struct AppState {
storage: Storage, db: DataBase,
template: tera::Tera, database_distribution: Uniform<usize>,
} }
fn get_question(storage: &Storage, id: u32) -> Result<Option<Question>, Error> { impl From<DataBaseInner> for AppState {
if 0 == id { fn from(db: DataBaseInner) -> Self {
return Ok(None); let last_id = db.len();
} let database_distribution = rand::distributions::Uniform::new_inclusive(1usize, last_id);
let db = Arc::new(db);
let collection = storage.collection("questions").unwrap(); Self {
let last_id = collection.last_id().unwrap(); db,
database_distribution,
if id > last_id {
Err(Error::from(()))
} else {
let question = collection.get::<Question>(id);
if question.is_err() {
Err(Error::from(()))
} else {
Ok(question.unwrap())
} }
} }
} }
fn show_question_details(template_file: &str, data: web::Data<AppState>, id: web::Path<u32>) -> Result<HttpResponse, Error> { fn random_question_id(database_distribution: &Uniform<usize>) -> usize {
let id = id.into_inner();
let question = get_question(&data.storage, id);
if question.is_ok() {
let question = question.unwrap();
if question.is_some() {
let question = question.unwrap();
let body = data.template.render(template_file, &question).unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(body))
} else {
Ok(HttpResponse::Found()
.header(header::LOCATION, "/q/")
.finish())
}
} else {
let context = Context::new();
Ok(HttpResponse::with_body(
actix_web::http::StatusCode::NOT_FOUND,
actix_web::dev::Body::from(data.template.render("404.html", &context).unwrap()),
))
}
}
fn show_question(data: web::Data<AppState>, id: web::Path<u32>) -> Result<HttpResponse, Error> {
show_question_details("question.html", data, id)
}
fn show_answer(data: web::Data<AppState>, id: web::Path<u32>) -> Result<HttpResponse, Error> {
show_question_details("answer.html", data, id)
}
fn index(data: web::Data<AppState>, req: HttpRequest) -> Result<HttpResponse, Error> {
let collection = data.storage.collection("questions").unwrap();
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let last_id = collection.last_id().unwrap(); rng.sample(database_distribution)
let id = (1..(last_id + 1)).choose(&mut rng).unwrap();
let url = req.url_for("question", &[format!("{}", id)])?;
Ok(HttpResponse::Found()
.header(header::LOCATION, url.as_str())
.finish())
} }
fn main() { async fn get_question(db: &DataBase, id: usize) -> Result<Question, ()> {
std::env::set_var("RUST_LOG", "actix_web=info"); db.get(id - 1).await.err_empty()
env_logger::init(); }
let options: Options = serde_json::from_value(json!({ async fn get_question_data(
"read_only": true, data: &AppState,
"no_lock": true, cache: &TemplateCache,
})) id: usize,
.unwrap(); ) -> Option<ArcTemplateData> {
if let Some(value) = cache.get(&id) {
let storage = Storage::new("db", options).unwrap(); return Some(value);
}
HttpServer::new(move || {
let data = AppState { match get_question(&data.db, id).await {
storage: storage.clone(), Ok(question) => {
template: compile_templates!("./templates/**/*"), let mut context = tera::to_value(question).expect("question serialize");
}; if context.is_object() {
App::new() let next_id = random_question_id(&data.database_distribution);
.wrap(Logger::default()) context["next"] = tera::to_value(next_id).expect("question id serialize");
.data(data) }
.route("/q", web::to(index))
.service( let data = ArcTemplateData::new(context);
web::scope("/q") cache.insert(id, data.clone());
.service(actix_files::Files::new("/static", "./static")) Some(data)
.service( }
web::resource("/{id}") Err(_) => None,
.name("question") // <- set resource name, then it could be used in `url_for` }
.guard(guard::Get()) }
.to(show_question),
) async fn show_question_details(
.service( template_name: &'static str,
web::resource("/{id}/a/") data: &AppState,
.name("answer") // <- set resource name, then it could be used in `url_for` cache: &TemplateCache,
.guard(guard::Get()) id: usize,
.to(show_answer), ) -> Option<Template> {
) get_question_data(data, cache, id)
.route("/", web::to(index)) .await
) .map(|data| data.render(template_name))
.route("/", web::to(index)) }
})
.bind("127.0.0.1:8088") #[get("/q/<id>/json")]
.unwrap() async fn json_question(
.run() data: &State<AppState>,
.unwrap(); cache: &State<TemplateCache>,
id: usize,
) -> Option<ArcTemplateData> {
get_question_data(data.inner(), cache.inner(), id).await
}
#[get("/q/<id>")]
async fn show_question(
data: &State<AppState>,
cache: &State<TemplateCache>,
id: usize,
) -> Option<Template> {
show_question_details("question", data.inner(), cache.inner(), id).await
}
#[get("/q/<id>/a")]
async fn show_answer(
data: &State<AppState>,
cache: &State<TemplateCache>,
id: usize,
) -> Option<Template> {
show_question_details("answer", data.inner(), cache.inner(), id).await
}
#[get("/q/0")]
fn question0() -> Redirect {
Redirect::to("/")
}
#[get("/q/0/a")]
fn answer0() -> Redirect {
Redirect::to("/")
}
#[get("/q/0/json")]
fn json0(data: &State<AppState>) -> Redirect {
let id = random_question_id(&data.database_distribution);
Redirect::temporary(format!("/q/{}/json", id))
}
#[get("/")]
fn index(data: &State<AppState>) -> Redirect {
let id = random_question_id(&data.database_distribution);
Redirect::temporary(format!("/q/{}", id))
}
#[catch(404)]
fn not_found(_req: &rocket::Request) -> WebError {
WebError::not_found()
}
#[catch(500)]
fn server_error(_req: &rocket::Request) -> WebError {
WebError::server_error()
}
#[launch]
async fn rocket() -> _ {
let state: AppState = async_db::Reader::new(DB_FILENAME)
.await
.expect("open db")
.into();
let cache: TemplateCache = Cache::builder()
.time_to_idle(Duration::from_secs(15 * 60))
.max_capacity(300)
.build();
rocket::build()
.manage(state)
.manage(cache)
.register("/", catchers![not_found, server_error])
.mount(
"/",
routes![
index,
json_question,
show_question,
show_answer,
question0,
answer0,
json0
],
)
.mount("/q", routes![index])
.mount("/q/static", FileServer::new("static/", Options::None))
.attach(Template::fairing())
} }

7
static/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -9,3 +9,30 @@ body {
.content-block-inner { .content-block-inner {
margin: auto; margin: auto;
} }
.footer {
text-align: center;
z-index: -1;
}
details {
text-align: center;
}
#details {
text-align: justify;
}
.nav-link {
margin-top: 0px;
margin-bottom: 0px;
padding-top: 0px;
padding-bottom: 0px;
}
.navbar {
margin-top: 0px;
margin-bottom: 0px;
padding-top: 0px;
padding-bottom: 0px;
}

View File

@@ -1,5 +0,0 @@
{% extends "base.html" %}
{% block title %}404{% endblock title %}
{% block content %}
<h1>404 - Could not find that page</h1>
{% endblock content %}

13
templates/404.html.tera Normal file
View File

@@ -0,0 +1,13 @@
{% extends "base" %}
{% block title %}404{% endblock title %}
{% block nav %}
{% endblock nav %}
{% block content %}
<div class="content-block">
<div class="content-block-inner mt-4 px-4 py-2 fs-1 top-50 start-50 translate-middle">
<h1 class="display-4 d-flex justify-content-center text-center fw-bold">404</h1>
<br/>
<p>Нет такой страницы &#128532;</p>
</div>
</div>
{% endblock content %}

13
templates/500.html.tera Normal file
View File

@@ -0,0 +1,13 @@
{% extends "base" %}
{% block title %}404{% endblock title %}
{% block nav %}
{% endblock nav %}
{% block content %}
<div class="content-block">
<div class="content-block-inner mt-4 px-4 py-2 fs-1 top-50 start-50 translate-middle">
<h1 class="display-4 d-flex justify-content-center text-center fw-bold">500</h1>
<br/>
<p>&#128558; Ой, кажется у нас тут что то сломалось...</p>
</div>
</div>
{% endblock content %}

View File

@@ -1,23 +1,22 @@
{% extends "base.html" %} {% block title %} Ответ {% endblock title %} {% extends "base" %} {% block title %} Ответ {% endblock title %}
{% block nav %}
<li class="nav-item"><a class="nav-link text-primary" href="/q/{{ next }}">Ещё</a></li>
{% endblock nav %}
{% block content %} {% block content %}
<!-- <h1>{{ id }}</h1> --> <!-- <h1>{{ id }}</h1> -->
<div class="content-block"> <div class="content-block">
<div id="question" class="content-block-inner"> <div id="question" class="content-block-inner">
<p><font color="#544669"> <p>
<h4> {{ description }} </h4> <h4 class="text-muted"> {{ description }} </h4>
</font>
</p> </p>
</div> </div>
</div><br /> </div><br />
<div class="content-block"> <div class="content-block">
<div id="answer" class="content-block-inner"> <div id="answer" class="content-block-inner">
<hr/> <div class="mt-2 px-2 py-1 shadow rounded">
<p> <!-- <h2> Ответ: </h2> -->
<!-- <h2> Ответ: </h2> --> <h1 class="display-4 d-flex justify-content-center text-center">{{ answer }}</h1>
<h1>{{ answer }}</h1> </div><br><br/><details>
</p>
<hr/><br><br/><details>
<div id="details"> <div id="details">
{% if comment | length or comment1 | length %} {% if comment | length or comment1 | length %}
<p><span>Комментарии:</span> <p><span>Комментарии:</span>
@@ -43,7 +42,7 @@
<p><span>Тема: </span> {{ theme }}</p> <p><span>Тема: </span> {{ theme }}</p>
{% endif %} {% endif %}
{% if rating | length %} {% if rating | length %}
<p><span>Рейтинг: </span> {{ author }}</p> <p><span>Рейтинг: </span> {{ rating }}</p>
{% endif %} {% endif %}
{% if batch_info.description | length %} {% if batch_info.description | length %}
<p><span>Чемпионат: </span> {{ batch_info.description }}</p> <p><span>Чемпионат: </span> {{ batch_info.description }}</p>
@@ -57,4 +56,4 @@
</div></details> </div></details>
</div> </div>
<br /> <br />
{% endblock content %} {% endblock content %}

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="/q/static/style.css" />
<title>{% block title %}{% endblock title %}</title>
</head>
<body>
{% include "nav.html" %}
<div id="content" class="container">{% block content %}{% endblock content %}</div>
</body>
</html>

27
templates/base.html.tera Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" type="text/css" href="/q/static/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/q/static/style.css" />
<title>{% block title %}{% endblock title %}</title>
</head>
<body>
<header class="shadow-sm rounded-bottom">
<nav class="navbar navbar-expand-sm container sticky-top navbar-light justify-content-center">
<ul class="nav navbar-nav">
{% include "nav" %}
{% block nav %}{% endblock nav %}
</ul>
</nav>
</header>
<article class="mb-5 pb-5">
<div id="content" class="container">{% block content %}{% endblock content %}</div>
</article>
<footer class="footer fixed-bottom shadow-lg rounded-top">
<p class="my-0 py-0 text-secondary">Источник: <a class="text-secondary" href="https://db.chgk.info/">db.chgk.info</a></p>
</footer>
<!-- Yandex.Metrika counter --> <script type="text/javascript" > (function (d, w, c) { (w[c] = w[c] || []).push(function() { try { w.yaCounter48214757 = new Ya.Metrika2({ id:48214757, clickmap:true, trackLinks:true, accurateTrackBounce:true, trackHash:true, ut:"noindex" }); } catch(e) { } }); var n = d.getElementsByTagName("script")[0], s = d.createElement("script"), f = function () { n.parentNode.insertBefore(s, n); }; s.type = "text/javascript"; s.async = true; s.src = "https://mc.yandex.ru/metrika/tag.js"; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "yandex_metrika_callbacks2"); </script> <noscript><div><img src="https://mc.yandex.ru/watch/48214757?ut=noindex" style="position:absolute; left:-9999px;" alt="" /></div></noscript> <!-- /Yandex.Metrika counter -->
</body>
</html>

View File

@@ -1,3 +0,0 @@
<nav class="nav nav-pills container justify-content-center">
<a class="nav-link" href="/q/">Ещё</a>
</nav>

1
templates/nav.html.tera Normal file
View File

@@ -0,0 +1 @@
<li class="nav-item"><a class="nav-link text-info" href="/">Главная</a></li>

View File

@@ -1,17 +0,0 @@
{% extends "base.html" %} {% block title %} Вопрос {% endblock title %}
{% block content %}
<!-- <h1>{{ id }}</h1> -->
<div class="content-block"><div id="question" class="content-block-inner">
<p>
<h3> {{ description }} </h3>
</p>
</div></div>
<br/>
<br/>
<br/><details>
<nav class="nav nav-pills container justify-content-center">
<a class="nav-link" href="/q/{{ num }}/a/">Ответ</a>
</nav></details>
{% endblock content %}

View File

@@ -0,0 +1,18 @@
{% extends "base" %} {% block title %} Вопрос {% endblock title %}
{% block nav %}
<li class="nav-item"><a class="nav-link text-primary" href="/q/{{ next }}">Ещё</a></li>
{% endblock nav %}
{% block content %}
<!-- <h1>{{ id }}</h1> -->
<div class="content-block"><div id="question" class="content-block-inner">
<p>
<h3> {{ description }} </h3>
</p>
</div></div>
<br/>
<br/>
<br/>
<div class="justify-content-center text-center">
<a class="btn btn-dark shadow" href="/q/{{ num }}/a/">Показать ответ</a>
</div>
{% endblock content %}