4 Commits

Author SHA1 Message Date
b4tman aec2e87472 + viewport meta 2019-08-19 22:03:06 +03:00
b4tman 0bea76eac7 redesign 2019-08-19 19:24:07 +03:00
b4tman 80ee37c2ee saved distribution for rand 2019-08-19 19:03:35 +03:00
b4tman d97b820363 upd 2019-08-19 18:40:34 +03:00
17 changed files with 1914 additions and 1614 deletions
-12
View File
@@ -1,12 +0,0 @@
kind: pipeline
name: default
steps:
- name: build
image: rustlang/rust:nightly-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 /target
**/*.rs.bk **/*.rs.bk
db.dat
/db /db
Generated
+1568 -1342
View File
File diff suppressed because it is too large Load Diff
+12 -14
View File
@@ -1,28 +1,26 @@
[package] [package]
name = "qchgk_web" name = "qchgk_web"
version = "0.1.1" version = "0.1.0"
authors = ["Dmitry <b4tm4n@mail.ru>"] authors = ["Dmitry <b4tm4n@mail.ru>"]
edition = "2021" edition = "2018"
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]
rand="0.8" actix-files="0.1"
actix-web = "1.0"
serde="1.0" serde="1.0"
serde_derive="1.0"
serde_json="1.0" serde_json="1.0"
rocket="0.4" ledb="0.2"
chgk_ledb_lib = {git = "https://gitea.b4tman.ru/b4tman/chgk_ledb.git", rev="8120a996a3", package="chgk_ledb_lib"} ledb-derive="0.2"
ledb-types="0.2"
[dependencies.rocket_contrib] lmdb-zero="0.4"
version = "0.4" rand="0.7"
default-features = false env_logger = "0.6"
features = ["serve", "tera_templates"] tera = "0.11"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
debug = false debug = false
lto = true lto = true
strip = true
-21
View File
@@ -1,21 +0,0 @@
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
@@ -1,10 +0,0 @@
# 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
+197 -90
View File
@@ -1,134 +1,241 @@
#![feature(proc_macro_hygiene, decl_macro)] extern crate actix_files;
extern crate actix_web;
extern crate serde; extern crate serde;
extern crate serde_json;
#[macro_use] #[macro_use]
extern crate rocket; extern crate serde_derive;
extern crate rocket_contrib; #[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;
use rocket::response::Redirect; use tera::Context;
use rocket::{Rocket, State};
use rocket_contrib::serve::StaticFiles;
use rocket_contrib::templates::Template; use actix_web::{
guard, http::header, middleware::Logger, web,
App, Error, HttpRequest, HttpResponse, HttpServer, Result,
};
use rand::distributions::Uniform; use rand::distributions::Uniform;
use rand::Rng; use rand::Rng;
use std::sync::Arc;
use chgk_ledb_lib::db; use ledb::{Options, Storage};
use chgk_ledb_lib::questions::Question;
const DB_FILENAME: &str = "db.dat"; #[derive(Debug, Default, Clone, Serialize, Deserialize, Document)]
struct BatchInfo {
trait ErrorEmpty { #[document(primary)]
type Output; #[serde(default)]
fn err_empty(self) -> Result<Self::Output, ()>; 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,
} }
impl<T, E> ErrorEmpty for Result<T, E> { #[derive(Debug, Default, Clone, Serialize, Deserialize, Document)]
type Output = T; struct Question {
fn err_empty(self) -> Result<Self::Output, ()> { #[document(primary)]
self.map_err(|_| ()) #[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,
} }
type DataBaseInner = db::Reader<Question>; struct AppState {
type DataBase = Arc<DataBaseInner>; storage: Storage,
struct AppState{ template: tera::Tera,
db: DataBase, database_distribution: Uniform<u32>,
database_distribution: Uniform<usize>,
} }
impl From<DataBaseInner> for AppState { fn get_database_distribution(storage: &Storage) -> Uniform<u32> {
fn from(db: DataBaseInner) -> Self { let collection = storage
let last_id = db.len(); .collection("questions")
let database_distribution = rand::distributions::Uniform::new_inclusive(1usize, last_id); .expect("collection \"questions\"");
let db = Arc::new(db); let last_id = collection.last_id().expect("\"questions\" last_id");
Self { rand::distributions::Uniform::new_inclusive(1u32, last_id)
db,
database_distribution
}
}
} }
fn random_question_id(database_distribution: &Uniform<usize>) -> usize { fn random_question_id(database_distribution: &Uniform<u32>) -> u32 {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
rng.sample(database_distribution) rng.sample(database_distribution)
} }
fn get_question(db: &DataBase, id: usize) -> Result<Question, ()> { fn get_question(storage: &Storage, id: u32) -> Result<Option<Question>, Error> {
db.get(id - 1).err_empty() 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())
}
}
} }
fn show_question_details(template_name: &'static str, data: &AppState, id: usize) -> Template { fn show_question_details(template_file: &str, data: web::Data<AppState>, id: web::Path<u32>) -> Result<HttpResponse, Error> {
match get_question(&data.db, id) { let id = id.into_inner();
Ok(question) => {
let question = get_question(&data.storage, id);
if question.is_ok() {
let question = question.unwrap();
if question.is_some() {
let question = question.unwrap();
let mut context = serde_json::to_value(question).expect("question serialize"); let mut context = serde_json::to_value(question).expect("question serialize");
if context.is_object() { if context.is_object() {
let next_id = random_question_id(&data.database_distribution); let next_id = random_question_id(&data.database_distribution);
context["next"] = serde_json::to_value(next_id).expect("question id serialize"); context["next"] = serde_json::to_value(next_id).expect("question id serialize");
} }
Template::render(template_name, &context) let body = data.template.render(template_file, &context).expect("template render - show_question_details");
} Ok(HttpResponse::Ok().content_type("text/html").body(body))
Err(_) => { } else {
use std::collections::HashMap; Ok(HttpResponse::PermanentRedirect()
let context: HashMap<String, String> = HashMap::new(); .header(header::LOCATION, "/q/")
Template::render("404", context) .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()),
))
} }
} }
#[get("/q/<id>")] fn show_question(data: web::Data<AppState>, id: web::Path<u32>) -> Result<HttpResponse, Error> {
fn show_question(data: State<AppState>, id: usize) -> Template { show_question_details("question.html", data, id)
show_question_details("question", data.inner(), id)
} }
#[get("/q/<id>/a")] fn show_answer(data: web::Data<AppState>, id: web::Path<u32>) -> Result<HttpResponse, Error> {
fn show_answer(data: State<AppState>, id: usize) -> Template { show_question_details("answer.html", data, id)
show_question_details("answer", data.inner(), id)
} }
#[get("/q/0")] fn index(data: web::Data<AppState>, _req: HttpRequest) -> Result<HttpResponse, Error> {
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 id = random_question_id(&data.database_distribution);
Redirect::temporary(format!("/q/{}", id))
}
#[catch(404)] let url = format!("/q/{}", id);
fn not_found(_req: &rocket::Request) -> Template {
use std::collections::HashMap;
let context: HashMap<String, String> = HashMap::new();
Template::render("404", context)
}
fn rocket() -> Rocket { Ok(HttpResponse::TemporaryRedirect()
let state: AppState = db::Reader::new(DB_FILENAME, 2048).expect("open db").into(); .header(header::LOCATION, url.as_str())
.finish())
rocket::ignite()
.manage(state)
.register(catchers![not_found])
.mount(
"/",
routes![index, show_question, show_answer, question0, answer0],
)
.mount("/q", routes![index])
.mount("/q/static", StaticFiles::from("static/"))
.attach(Template::fairing())
} }
fn main() { fn main() {
rocket().launch(); 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");
} }
+4 -4
View File
File diff suppressed because one or more lines are too long
+1 -14
View File
@@ -11,6 +11,7 @@ body {
} }
.footer { .footer {
widows: 100%;
text-align: center; text-align: center;
z-index: -1; z-index: -1;
} }
@@ -21,18 +22,4 @@ details {
#details { #details {
text-align: justify; 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" %} {% extends "base.html" %}
{% block title %}404{% endblock title %} {% block title %}404{% endblock title %}
{% block content %} {% block content %}
<h1>404 - Could not find that page</h1> <h1>404 - Could not find that page</h1>
+48
View File
@@ -0,0 +1,48 @@
{% 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
@@ -1,59 +0,0 @@
{% 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
@@ -0,0 +1,61 @@
<!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
@@ -1,27 +0,0 @@
<!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
@@ -0,0 +1,20 @@
{% 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
@@ -1,18 +0,0 @@
{% 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 %}