36 Commits

Author SHA1 Message Date
b4tman eda704d2f1 test concurent get q from db/cache
continuous-integration/drone/push Build is passing
2023-08-18 14:56:08 +03:00
b4tman da962a9f7a v0.2.0
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
continuous-integration/drone/push Build is passing
Reviewed-on: #3
2023-08-18 08:07:13 +00:00
b4tman 971c79a111 use async db
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-18 10:50:56 +03:00
b4tman a4dd460c37 use serde from rocket_dyn_templates
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-05 19:42:19 +03:00
b4tman 67087e6583 ci: add pipeline type
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is passing
2023-08-05 19:31:35 +03:00
b4tman ca75f84ee2 ci: fix img
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-05 19:25:50 +03:00
b4tman a951eff714 ci: use rust stable
continuous-integration/drone/push Build encountered an error
2023-08-05 19:24:47 +03:00
b4tman 18eb3ec51b min migrate to rocket 0.5
continuous-integration/drone/push Build is passing
2023-08-05 19:21:00 +03:00
b4tman 78d57ca2e6 update lockfile
continuous-integration/drone/push Build is passing
2023-08-05 17:50:49 +03:00
b4tman 103391c891 mini_moka
continuous-integration/drone/push Build is passing
2023-04-04 11:39:52 +03:00
b4tman 6d1d8eaea1 upd deps
continuous-integration/drone/push Build is passing
2023-04-03 15:43:30 +03:00
b4tman d1cb2187c1 add cache
continuous-integration/drone/push Build is passing
b4tman/qchgk_web#1
using moka
2023-04-02 17:01:51 +03:00
b4tman d9a060b1b7 add .drone.yml
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-03-29 09:34:01 +03:00
b4tman d6571959db Merge branch 'rocket' 2023-03-28 16:37:03 +03:00
b4tman b281f85a70 share database reader instance 2023-03-28 16:36:04 +03:00
b4tman 89735f98d3 upd chgk_ledb 2023-03-28 16:30:40 +03:00
b4tman f56ccf471f new db 2023-01-04 01:12:03 +03:00
b4tman 747a611a67 use chgk_ledb_lib 2022-11-11 20:56:35 +03:00
b4tman 1eadfac11a Merge branch 'rocket' 2022-10-04 00:20:27 +03:00
b4tman 1b833cff90 add README 2022-10-04 00:17:02 +03:00
b4tman 3097ef9f08 add LICENSE 2022-10-04 00:11:17 +03:00
b4tman 0f837db0a2 refactor show_question_details 2022-10-04 00:07:25 +03:00
b4tman b3fcc66b1b upd deps 2022-10-04 00:06:08 +03:00
b4tman 8231274658 back to rand 0.7 2021-02-06 17:38:25 +03:00
b4tman ee2e8cd560 upd + show answer button 2021-02-06 00:46:53 +03:00
b4tman eb6349b51b upd ledb to 0.4 2020-08-18 21:51:46 +03:00
b4tman 1c2e08663c deps update 2020-08-12 09:15:14 +03:00
b4tman 39fdf46037 redesign with shadows 2019-08-22 15:18:38 +03:00
b4tman 987593a48d upd bootstrap to 4.3.1 2019-08-22 14:19:14 +03:00
b4tman 40f9a399dc shrink-to-fit=no 2019-08-22 14:16:43 +03:00
b4tman afe12bd630 base template: add more space to bottom 2019-08-22 13:48:37 +03:00
b4tman 5c49405ac0 cargo update 2019-08-22 13:42:48 +03:00
b4tman 665bbaadfb + viewport meta
(cherry picked from commit aec2e87472)
2019-08-19 22:05:42 +03:00
b4tman fb40a5a862 redesign 2019-08-19 15:58:47 +03:00
b4tman 2a6cd616be from actix to rocket 2019-08-05 17:06:27 +03:00
17 changed files with 2199 additions and 1963 deletions
+13
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
+2 -2
View File
@@ -1,4 +1,4 @@
/target
**/*.rs.bk
/db
db.dat
/db
Generated
+1866 -1611
View File
File diff suppressed because it is too large Load Diff
+12 -14
View File
@@ -1,26 +1,24 @@
[package]
name = "qchgk_web"
version = "0.1.0"
version = "0.2.0"
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
[dependencies]
actix-files="0.1"
actix-web = "1.0"
serde="1.0"
serde_derive="1.0"
serde_json="1.0"
ledb="0.2"
ledb-derive="0.2"
ledb-types="0.2"
lmdb-zero="0.4"
rand="0.7"
env_logger = "0.6"
tera = "0.11"
rand="0.8"
rocket = { version = "=0.5.0-rc.3", features = ["json"] }
rocket_dyn_templates = { version = "=0.1.0-rc.3", features = ["tera"] }
chgk_ledb_lib = {git = "https://gitea.b4tman.ru/b4tman/chgk_ledb.git", rev="699478f85e", package="chgk_ledb_lib", features=["async"]}
mini-moka = "0.10.0"
[profile.release]
opt-level = 3
debug = false
lto = true
strip = true
+21
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
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
+152 -201
View File
@@ -1,241 +1,192 @@
extern crate actix_files;
extern crate actix_web;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
extern crate ledb;
#[macro_use]
extern crate ledb_derive;
extern crate env_logger;
extern crate ledb_types;
#[macro_use]
extern crate tera;
extern crate rocket;
use tera::Context;
use actix_web::{
guard, http::header, middleware::Logger, web,
App, Error, HttpRequest, HttpResponse, HttpServer, Result,
};
use rocket::fs::FileServer;
use rocket::futures::FutureExt;
use rocket::response::Redirect;
use rocket::State;
use rocket_dyn_templates::tera;
use rocket_dyn_templates::Template;
use rand::distributions::Uniform;
use rand::Rng;
use std::ops::Deref;
use std::sync::Arc;
use ledb::{Options, Storage};
use chgk_ledb_lib::async_db;
use chgk_ledb_lib::questions::Question;
#[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,
use mini_moka::sync::Cache;
use std::time::Duration;
const DB_FILENAME: &str = "db.dat";
trait ErrorEmpty {
type Output;
fn err_empty(self) -> Result<Self::Output, ()>;
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, Document)]
struct Question {
#[document(primary)]
#[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(Clone)]
struct ArcTemplateData {
value: Arc<tera::Value>,
}
impl ArcTemplateData {
fn new(value: tera::Value) -> ArcTemplateData {
ArcTemplateData {
value: Arc::new(value),
}
}
fn render(&self, name: &'static str) -> Template {
Template::render(name, self.value.deref())
}
}
type TemplateCache = mini_moka::sync::Cache<usize, ArcTemplateData>;
type DataBaseInner = async_db::Reader<Question>;
type DataBase = Arc<DataBaseInner>;
struct AppState {
storage: Storage,
template: tera::Tera,
database_distribution: Uniform<u32>,
db: DataBase,
database_distribution: Uniform<usize>,
}
fn get_database_distribution(storage: &Storage) -> Uniform<u32> {
let collection = storage
.collection("questions")
.expect("collection \"questions\"");
let last_id = collection.last_id().expect("\"questions\" last_id");
impl From<DataBaseInner> for AppState {
fn from(db: DataBaseInner) -> Self {
let last_id = db.len();
let database_distribution = rand::distributions::Uniform::new_inclusive(1usize, last_id);
let db = Arc::new(db);
rand::distributions::Uniform::new_inclusive(1u32, last_id)
Self {
db,
database_distribution,
}
}
}
fn random_question_id(database_distribution: &Uniform<u32>) -> u32 {
fn random_question_id(database_distribution: &Uniform<usize>) -> usize {
let mut rng = rand::thread_rng();
rng.sample(database_distribution)
}
fn get_question(storage: &Storage, id: u32) -> Result<Option<Question>, Error> {
if 0 == id {
return Ok(None);
}
let collection = storage.collection("questions").expect("collection questions");
let last_id = collection.last_id().expect("questions last id");
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())
}
}
async fn get_question(db: &DataBase, id: usize) -> Result<Question, ()> {
db.get(id - 1).await.err_empty()
}
fn show_question_details(template_file: &str, data: web::Data<AppState>, id: web::Path<u32>) -> Result<HttpResponse, Error> {
let id = id.into_inner();
async fn show_question_details(
template_name: &'static str,
data: &AppState,
cache: &TemplateCache,
id: usize,
) -> Template {
let cache_cloned = cache.clone();
let question = get_question(&data.storage, id);
let from_cache = rocket::tokio::spawn(async move { cache_cloned.get(&id) }).fuse();
let from_db = async move {
match get_question(&data.db, id).await {
Ok(question) => {
let mut context = tera::to_value(question).expect("question serialize");
if context.is_object() {
let next_id = random_question_id(&data.database_distribution);
context["next"] = tera::to_value(next_id).expect("question id serialize");
}
if question.is_ok() {
let question = question.unwrap();
let value = ArcTemplateData::new(context);
let result = value.render(template_name);
cache.insert(id, value);
if question.is_some() {
let question = question.unwrap();
let mut context = serde_json::to_value(question).expect("question serialize");
if context.is_object() {
let next_id = random_question_id(&data.database_distribution);
context["next"] = serde_json::to_value(next_id).expect("question id serialize");
result
}
Err(_) => {
use std::collections::HashMap;
let context: HashMap<String, String> = HashMap::new();
Template::render("404", context)
}
}
}
.fuse();
loop {
rocket::tokio::select! {
biased;
Ok(Some(template)) = from_cache => {
println!("from cache");
break template.render(template_name)
},
template = from_db => {
println!("from db");
break template
}
let body = data.template.render(template_file, &context).expect("template render - show_question_details");
Ok(HttpResponse::Ok().content_type("text/html").body(body))
} else {
Ok(HttpResponse::PermanentRedirect()
.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)
#[get("/q/<id>")]
async fn show_question(
data: &State<AppState>,
cache: &State<TemplateCache>,
id: usize,
) -> Template {
show_question_details("question", data.inner(), cache.inner(), id).await
}
fn show_answer(data: web::Data<AppState>, id: web::Path<u32>) -> Result<HttpResponse, Error> {
show_question_details("answer.html", data, id)
#[get("/q/<id>/a")]
async fn show_answer(data: &State<AppState>, cache: &State<TemplateCache>, id: usize) -> Template {
show_question_details("answer", data.inner(), cache.inner(), id).await
}
fn index(data: web::Data<AppState>, _req: HttpRequest) -> Result<HttpResponse, Error> {
#[get("/q/0")]
fn question0() -> Redirect {
Redirect::to("/")
}
#[get("/q/0/a")]
fn answer0() -> Redirect {
Redirect::to("/")
}
#[get("/")]
fn index(data: &State<AppState>) -> Redirect {
let id = random_question_id(&data.database_distribution);
let url = format!("/q/{}", id);
Ok(HttpResponse::TemporaryRedirect()
.header(header::LOCATION, url.as_str())
.finish())
Redirect::temporary(format!("/q/{}", id))
}
fn main() {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
let options: Options = serde_json::from_value(json!({
"read_only": true,
"no_lock": true,
}))
.expect("options json parse");
let storage = Storage::new("db", options).expect("db open");
HttpServer::new(move || {
let mut data = AppState {
storage: storage.clone(),
template: compile_templates!("./templates/**/*"),
database_distribution: Uniform::new(1, 3),
};
data.database_distribution = get_database_distribution(&data.storage);
let data = data;
App::new()
.wrap(Logger::default())
.data(data)
.service(actix_files::Files::new("/static", "./static"))
.route("/q", web::to(index))
.service(
web::scope("/q")
.service(actix_files::Files::new("/static", "./static"))
.service(
web::resource("/{id}")
.name("question")
.guard(guard::Get())
.to(show_question),
)
.service(
web::resource("/{id}/a/")
.name("answer")
.guard(guard::Get())
.to(show_answer),
)
.route("/", web::to(index))
)
.route("/", web::to(index))
})
.bind("0.0.0.0:8088")
.expect("HttpServer::bind to 8088")
.run()
.expect("HttpServer::run");
#[catch(404)]
fn not_found(_req: &rocket::Request) -> Template {
use std::collections::HashMap;
let context: HashMap<String, String> = HashMap::new();
Template::render("404", context)
}
#[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])
.mount(
"/",
routes![index, show_question, show_answer, question0, answer0],
)
.mount("/q", routes![index])
.mount("/q/static", FileServer::from("static/"))
.attach(Template::fairing())
}
+4 -4
View File
File diff suppressed because one or more lines are too long
+14 -1
View File
@@ -11,7 +11,6 @@ body {
}
.footer {
widows: 100%;
text-align: center;
z-index: -1;
}
@@ -22,4 +21,18 @@ details {
#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;
}
@@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "base" %}
{% block title %}404{% endblock title %}
{% block content %}
<h1>404 - Could not find that page</h1>
-48
View File
@@ -1,48 +0,0 @@
{% extends "base.html" %} {% 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>
<font color="#544669">
<h4> {{ description }} </h4>
</font>
</p>
</div>
</div><br />
<div class="content-block">
<div id="answer" class="content-block-inner">
<hr/>
<p>
<!-- <h2> Ответ: </h2> -->
<h1 class="d-flex justify-content-center text-center">{{ answer }}</h1>
</p>
<hr/><br><br/>
<details>
<div id="details">
{% if comment | length or comment1 | length %}
<p><span>Комментарии:</span> {% if comment | length %} {{ comment }} {% endif %} {% if comment1 | length %}
<br/> {{ comment1 }} {% endif %}
</p>
{% endif %} {% if author | length %}
<p><span>Автор: </span> {{ author }}</p>
{% endif %} {% if copyright | length %}
<p><span>Копирайт: </span> {{ copyright }}</p>
{% endif %} {% if source | length %}
<p><span>Источник: </span> {{ source }}</p>
{% endif %} {% if theme | length %}
<p><span>Тема: </span> {{ theme }}</p>
{% endif %} {% if rating | length %}
<p><span>Рейтинг: </span> {{ author }}</p>
{% endif %} {% if batch_info.description | length %}
<p><span>Чемпионат: </span> {{ batch_info.description }}</p>
{% endif %} {% if tour | length %}
<p><span>Тур: </span> {{ tour }}</p>
{% endif %} {% if id | length %}
<p><span>Номер: </span> {{ id }}</p>
{% endif %}
</div>
</details>
</div>
<br /> {% endblock content %}
+59
View File
@@ -0,0 +1,59 @@
{% 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>
<h4 class="text-muted"> {{ description }} </h4>
</p>
</div>
</div><br />
<div class="content-block">
<div id="answer" class="content-block-inner">
<div class="mt-2 px-2 py-1 shadow rounded">
<!-- <h2> Ответ: </h2> -->
<h1 class="display-4 d-flex justify-content-center text-center">{{ answer }}</h1>
</div><br><br/><details>
<div id="details">
{% if comment | length or comment1 | length %}
<p><span>Комментарии:</span>
{% if comment | length %}
{{ comment }}
{% endif %}
{% if comment1 | length %}
<br/>
{{ comment1 }}
{% endif %}
</p>
{% endif %}
{% if author | length %}
<p><span>Автор: </span> {{ author }}</p>
{% endif %}
{% if copyright | length %}
<p><span>Копирайт: </span> {{ copyright }}</p>
{% endif %}
{% if source | length %}
<p><span>Источник: </span> {{ source }}</p>
{% endif %}
{% if theme | length %}
<p><span>Тема: </span> {{ theme }}</p>
{% endif %}
{% if rating | length %}
<p><span>Рейтинг: </span> {{ author }}</p>
{% endif %}
{% if batch_info.description | length %}
<p><span>Чемпионат: </span> {{ batch_info.description }}</p>
{% endif %}
{% if tour | length %}
<p><span>Тур: </span> {{ tour }}</p>
{% endif %}
{% if id | length %}
<p><span>Номер: </span> {{ id }}</p>
{% endif %}
</div></details>
</div>
<br />
{% endblock content %}
-61
View File
@@ -1,61 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>
<nav class="navbar navbar-expand-sm container sticky-top navbar-light justify-content-center">
<ul class="nav navbar-nav">
{% include "nav.html" %} {% block nav %}{% endblock nav %}
</ul>
</nav>
<hr/>
</header>
<article>
<div id="content" class="container">{% block content %}{% endblock content %}</div>
</article>
<footer class="footer fixed-bottom">
<hr/>
<p class="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>
+27
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>
-20
View File
@@ -1,20 +0,0 @@
{% extends "base.html" %} {% 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/>
<details>
<summary>Ответ</summary><br/>
<a class="btn btn-dark" href="/q/{{ num }}/a/">Показать ответ</a>
</details>
{% endblock content %}
+18
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 %}