Globální bannery
Tento commit je obsažen v:
rodič
1eeffc71c6
revize
bde9a95bd6
@ -1 +0,0 @@
|
||||
DROP TABLE accounts, boards, bans;
|
6
migrations/20231216092451_global_banners.sql
Normální soubor
6
migrations/20231216092451_global_banners.sql
Normální soubor
@ -0,0 +1,6 @@
|
||||
ALTER TABLE boards DROP COLUMN banners;
|
||||
|
||||
CREATE TABLE banners (
|
||||
id SERIAL NOT NULL PRIMARY KEY,
|
||||
banner JSONB NOT NULL
|
||||
);
|
@ -31,7 +31,6 @@ pub struct ServerCfg {
|
||||
pub struct SiteCfg {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub site_banner: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
|
64
src/db/banner.rs
Normální soubor
64
src/db/banner.rs
Normální soubor
@ -0,0 +1,64 @@
|
||||
use redis::AsyncCommands;
|
||||
use sqlx::{query, query_as, types::Json};
|
||||
|
||||
use super::models::{Banner, File};
|
||||
use crate::{ctx::Ctx, NekrochanError};
|
||||
|
||||
impl Banner {
|
||||
pub async fn create(ctx: &Ctx, banner: File) -> Result<Self, NekrochanError> {
|
||||
let banner: Self = query_as("INSERT INTO banners (banner) VALUES ($1) RETURNING *")
|
||||
.bind(Json(banner))
|
||||
.fetch_one(ctx.db())
|
||||
.await?;
|
||||
|
||||
ctx.cache()
|
||||
.zadd("banners", serde_json::to_string(&banner)?, banner.id)
|
||||
.await?;
|
||||
|
||||
Ok(banner)
|
||||
}
|
||||
|
||||
pub async fn read(ctx: &Ctx, id: i32) -> Result<Option<Self>, NekrochanError> {
|
||||
let banners: Vec<String> = ctx.cache().zrangebyscore("banners", id, id).await?;
|
||||
let json = banners.get(0);
|
||||
|
||||
let banner = match json {
|
||||
Some(json) => Some(serde_json::from_str(json)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(banner)
|
||||
}
|
||||
|
||||
pub async fn read_all(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
||||
let banners_str: Vec<String> = ctx.cache().zrange("banners", 0, -1).await?;
|
||||
let banners_json = format!("[{}]", banners_str.join(",")); // If it works, it works
|
||||
let banners = serde_json::from_str(&banners_json)?;
|
||||
|
||||
Ok(banners)
|
||||
}
|
||||
|
||||
pub async fn read_random(ctx: &Ctx) -> Result<Option<Self>, NekrochanError> {
|
||||
let banner: Option<String> = ctx.cache().zrandmember("banners", None).await?;
|
||||
|
||||
let banner = match banner {
|
||||
Some(json) => Some(serde_json::from_str(&json)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(banner)
|
||||
}
|
||||
|
||||
pub async fn remove(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
||||
self.banner.delete().await;
|
||||
|
||||
query("DELETE FROM banners WHERE id = $1")
|
||||
.bind(self.id)
|
||||
.execute(ctx.db())
|
||||
.await?;
|
||||
|
||||
ctx.cache().zrembyscore("banners", self.id, self.id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
use captcha::{gen, Difficulty};
|
||||
use rand::{seq::SliceRandom, thread_rng};
|
||||
use redis::{cmd, AsyncCommands, JsonAsyncCommands};
|
||||
use sha256::digest;
|
||||
use sqlx::{query, query_as, types::Json};
|
||||
@ -207,12 +206,6 @@ impl Board {
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn random_banner(&self) -> Option<File> {
|
||||
self.banners
|
||||
.choose(&mut thread_rng())
|
||||
.map(std::clone::Clone::clone)
|
||||
}
|
||||
|
||||
pub fn thread_captcha(&self) -> Option<(String, String)> {
|
||||
let captcha = match self.config.thread_captcha.as_str() {
|
||||
"easy" => gen(Difficulty::Easy),
|
||||
|
@ -3,7 +3,7 @@ use redis::{cmd, AsyncCommands, JsonAsyncCommands};
|
||||
use sha256::digest;
|
||||
use sqlx::query_as;
|
||||
|
||||
use super::models::{Account, Board, Post};
|
||||
use super::models::{Account, Banner, Board, Post};
|
||||
use crate::ctx::Ctx;
|
||||
|
||||
pub async fn init_cache(ctx: &Ctx) -> Result<(), Error> {
|
||||
@ -29,6 +29,16 @@ pub async fn init_cache(ctx: &Ctx) -> Result<(), Error> {
|
||||
ctx.cache().lpush("board_ids", &board.id).await?;
|
||||
}
|
||||
|
||||
let banners: Vec<Banner> = query_as("SELECT * FROM banners")
|
||||
.fetch_all(ctx.db())
|
||||
.await?;
|
||||
|
||||
for banner in &banners {
|
||||
ctx.cache()
|
||||
.zadd("banners", serde_json::to_string(banner)?, banner.id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
cmd("SORT")
|
||||
.arg("board_ids")
|
||||
.arg("ALPHA")
|
||||
|
@ -3,5 +3,6 @@ pub mod models;
|
||||
|
||||
mod account;
|
||||
mod ban;
|
||||
mod banner;
|
||||
mod board;
|
||||
mod post;
|
||||
|
@ -7,7 +7,7 @@ use sqlx::{types::Json, FromRow};
|
||||
|
||||
use crate::cfg::BoardCfg;
|
||||
|
||||
#[derive(FromRow, Clone, Serialize, Deserialize)]
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone)]
|
||||
pub struct Account {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
@ -21,7 +21,6 @@ pub struct Board {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub banners: Json<Vec<File>>,
|
||||
pub config: Json<BoardCfg>,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
@ -90,3 +89,9 @@ pub struct File {
|
||||
pub timestamp: i64,
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct Banner {
|
||||
pub id: i32,
|
||||
pub banner: Json<File>,
|
||||
}
|
||||
|
@ -333,6 +333,10 @@ impl Post {
|
||||
.await?;
|
||||
|
||||
for post in &to_be_deleted {
|
||||
for file in post.files.iter() {
|
||||
file.delete().await;
|
||||
}
|
||||
|
||||
let id = post.id;
|
||||
let url = post.post_url();
|
||||
|
||||
@ -381,7 +385,7 @@ impl Post {
|
||||
}
|
||||
|
||||
pub async fn delete_files(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
||||
for file in self.files.iter() {
|
||||
for file in &self.files.0 {
|
||||
file.delete().await;
|
||||
}
|
||||
|
||||
|
15
src/files.rs
15
src/files.rs
@ -11,7 +11,7 @@ use tokio::{
|
||||
use crate::{
|
||||
cfg::Cfg,
|
||||
ctx::Ctx,
|
||||
db::models::{Board, File, Post},
|
||||
db::models::{Banner, Board, File, Post},
|
||||
error::NekrochanError,
|
||||
};
|
||||
|
||||
@ -310,13 +310,18 @@ pub async fn cleanup_files(ctx: &Ctx) -> Result<(), Error> {
|
||||
let mut keep = HashSet::new();
|
||||
let mut keep_thumbs = HashSet::new();
|
||||
|
||||
let banners = Banner::read_all(ctx).await?;
|
||||
|
||||
for banner in banners {
|
||||
keep.insert(format!(
|
||||
"{}.{}",
|
||||
banner.banner.timestamp, banner.banner.format
|
||||
));
|
||||
}
|
||||
|
||||
let boards = Board::read_all(ctx).await?;
|
||||
|
||||
for board in boards {
|
||||
for file in board.banners.0 {
|
||||
keep.insert(format!("{}.{}", file.timestamp, file.format));
|
||||
}
|
||||
|
||||
let posts = Post::read_all(ctx, board.id.clone()).await?;
|
||||
|
||||
for post in posts {
|
||||
|
34
src/main.rs
34
src/main.rs
@ -3,11 +3,10 @@ use actix_web::{
|
||||
body::MessageBody,
|
||||
dev::ServiceResponse,
|
||||
get,
|
||||
http::StatusCode,
|
||||
http::header::{HeaderValue, CACHE_CONTROL, PRAGMA},
|
||||
middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||
post,
|
||||
web::Data,
|
||||
App, HttpResponse, HttpServer, ResponseError,
|
||||
App, HttpRequest, HttpResponse, HttpServer, ResponseError,
|
||||
};
|
||||
use anyhow::Error;
|
||||
use askama::Template;
|
||||
@ -15,7 +14,7 @@ use log::{error, info};
|
||||
use nekrochan::{
|
||||
cfg::Cfg,
|
||||
ctx::Ctx,
|
||||
db::cache::init_cache,
|
||||
db::{cache::init_cache, models::Banner},
|
||||
error::NekrochanError,
|
||||
files::cleanup_files,
|
||||
web::{self, template_response},
|
||||
@ -98,8 +97,8 @@ async fn run() -> Result<(), Error> {
|
||||
.service(web::staff::actions::update_board_config::update_board_config)
|
||||
.service(web::staff::actions::update_boards::update_boards)
|
||||
.service(web::staff::actions::update_permissions::update_permissions)
|
||||
.service(debug)
|
||||
.service(favicon)
|
||||
.service(random_banner)
|
||||
.service(Files::new("/static", "./static"))
|
||||
.service(Files::new("/uploads", "./uploads").disable_content_disposition())
|
||||
.wrap(ErrorHandlers::new().default_handler(error_handler))
|
||||
@ -118,11 +117,28 @@ async fn favicon() -> Result<NamedFile, NekrochanError> {
|
||||
Ok(favicon)
|
||||
}
|
||||
|
||||
#[post("/debug")]
|
||||
async fn debug(req: String) -> HttpResponse {
|
||||
println!("{req}");
|
||||
#[get("/random-banner")]
|
||||
async fn random_banner(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
|
||||
let file = if let Some(banner) = Banner::read_random(&ctx).await? {
|
||||
let timestamp = banner.banner.timestamp;
|
||||
let format = &banner.banner.format;
|
||||
|
||||
HttpResponse::new(StatusCode::OK)
|
||||
NamedFile::open(format!("./uploads/{timestamp}.{format}"))?
|
||||
} else {
|
||||
NamedFile::open("./static/default-banner.png")?
|
||||
};
|
||||
|
||||
let mut res = file.into_response(&req);
|
||||
|
||||
res.headers_mut().append(
|
||||
CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache, no-store, must-revalidate"),
|
||||
);
|
||||
|
||||
res.headers_mut()
|
||||
.append(PRAGMA, HeaderValue::from_static("no-cache"));
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
@ -59,7 +59,7 @@ impl PermissionWrapper {
|
||||
self.0.contains(Permissions::Bans)
|
||||
}
|
||||
|
||||
pub fn board_banners(&self) -> bool {
|
||||
pub fn banners(&self) -> bool {
|
||||
self.0.contains(Permissions::BoardBanners)
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,15 @@
|
||||
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
|
||||
use actix_multipart::form::{tempfile::TempFile, MultipartForm};
|
||||
use actix_web::{post, web::Data, HttpRequest, HttpResponse};
|
||||
|
||||
use crate::{
|
||||
ctx::Ctx,
|
||||
db::models::{Board, File},
|
||||
db::models::{Banner, File},
|
||||
error::NekrochanError,
|
||||
web::tcx::account_from_auth,
|
||||
};
|
||||
|
||||
#[derive(MultipartForm)]
|
||||
pub struct AddBannersForm {
|
||||
board: Text<String>,
|
||||
#[multipart(rename = "files[]")]
|
||||
files: Vec<TempFile>,
|
||||
}
|
||||
@ -23,32 +22,20 @@ pub async fn add_banners(
|
||||
) -> Result<HttpResponse, NekrochanError> {
|
||||
let account = account_from_auth(&ctx, &req).await?;
|
||||
|
||||
if !(account.perms().owner() || account.perms().board_banners()) {
|
||||
if !(account.perms().owner() || account.perms().banners()) {
|
||||
return Err(NekrochanError::InsufficientPermissionError);
|
||||
}
|
||||
|
||||
let board = form.board.0;
|
||||
let board = Board::read(&ctx, board.clone())
|
||||
.await?
|
||||
.ok_or(NekrochanError::BoardNotFound(board))?;
|
||||
|
||||
let mut new_banners = board.banners.0.clone();
|
||||
let added_banners = form.files;
|
||||
|
||||
let mut cfg = ctx.cfg.clone();
|
||||
|
||||
cfg.files.videos = false;
|
||||
|
||||
for banner in added_banners {
|
||||
let file = File::new(&cfg, banner, false, false).await?;
|
||||
|
||||
new_banners.push(file);
|
||||
for file in form.files {
|
||||
Banner::create(&ctx, File::new(&cfg, file, false, false).await?).await?;
|
||||
}
|
||||
|
||||
board.update_banners(&ctx, new_banners).await?;
|
||||
|
||||
let res = HttpResponse::SeeOther()
|
||||
.append_header(("Location", format!("/staff/banners/{}", board.id).as_str()))
|
||||
.append_header(("Location", "/staff/banners"))
|
||||
.finish();
|
||||
|
||||
Ok(res)
|
||||
|
@ -2,14 +2,14 @@ use actix_web::{post, web::Data, HttpRequest, HttpResponse};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth,
|
||||
ctx::Ctx, db::models::Banner, error::NekrochanError, qsform::QsForm,
|
||||
web::tcx::account_from_auth,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RemoveBannersForm {
|
||||
board: String,
|
||||
#[serde(default)]
|
||||
banners: Vec<usize>,
|
||||
banners: Vec<i32>,
|
||||
}
|
||||
|
||||
#[post("/staff/actions/remove-banners")]
|
||||
@ -20,30 +20,18 @@ pub async fn remove_banners(
|
||||
) -> Result<HttpResponse, NekrochanError> {
|
||||
let account = account_from_auth(&ctx, &req).await?;
|
||||
|
||||
if !(account.perms().owner() || account.perms().board_banners()) {
|
||||
if !(account.perms().owner() || account.perms().banners()) {
|
||||
return Err(NekrochanError::InsufficientPermissionError);
|
||||
}
|
||||
|
||||
let board = form.board;
|
||||
let board = Board::read(&ctx, board.clone())
|
||||
.await?
|
||||
.ok_or(NekrochanError::BoardNotFound(board))?;
|
||||
|
||||
let old_banners = board.banners.0.clone();
|
||||
let mut new_banners = Vec::new();
|
||||
|
||||
for (i, banner) in old_banners.into_iter().enumerate() {
|
||||
if form.banners.contains(&i) {
|
||||
banner.delete().await;
|
||||
} else {
|
||||
new_banners.push(banner);
|
||||
for id in form.banners {
|
||||
if let Some(banner) = Banner::read(&ctx, id).await? {
|
||||
banner.remove(&ctx).await?;
|
||||
}
|
||||
}
|
||||
|
||||
board.update_banners(&ctx, new_banners).await?;
|
||||
|
||||
let res = HttpResponse::SeeOther()
|
||||
.append_header(("Location", format!("/staff/banners/{}", board.id).as_str()))
|
||||
.append_header(("Location", "/staff/banners"))
|
||||
.finish();
|
||||
|
||||
Ok(res)
|
||||
|
@ -16,7 +16,7 @@ pub struct UpdatePermissionsForm {
|
||||
staff_log: Option<String>,
|
||||
reports: Option<String>,
|
||||
bans: Option<String>,
|
||||
board_banners: Option<String>,
|
||||
banners: Option<String>,
|
||||
board_config: Option<String>,
|
||||
bypass_bans: Option<String>,
|
||||
bypass_board_lock: Option<String>,
|
||||
@ -67,7 +67,7 @@ pub async fn update_permissions(
|
||||
permissions |= Permissions::Bans;
|
||||
}
|
||||
|
||||
if form.board_banners.is_some() {
|
||||
if form.banners.is_some() {
|
||||
permissions |= Permissions::BoardBanners;
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,9 @@
|
||||
use actix_web::{
|
||||
get,
|
||||
web::{Data, Path},
|
||||
HttpRequest, HttpResponse,
|
||||
};
|
||||
use actix_web::{get, web::Data, HttpRequest, HttpResponse};
|
||||
use askama::Template;
|
||||
|
||||
use crate::{
|
||||
ctx::Ctx,
|
||||
db::models::Board,
|
||||
db::models::Banner,
|
||||
error::NekrochanError,
|
||||
web::{
|
||||
tcx::{account_from_auth, TemplateCtx},
|
||||
@ -19,28 +15,20 @@ use crate::{
|
||||
#[template(path = "staff/banners.html")]
|
||||
struct BannersTemplate {
|
||||
tcx: TemplateCtx,
|
||||
board: Board,
|
||||
banners: Vec<Banner>,
|
||||
}
|
||||
|
||||
#[get("/staff/banners/{board}")]
|
||||
pub async fn banners(
|
||||
ctx: Data<Ctx>,
|
||||
req: HttpRequest,
|
||||
board: Path<String>,
|
||||
) -> Result<HttpResponse, NekrochanError> {
|
||||
#[get("/staff/banners")]
|
||||
pub async fn banners(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
|
||||
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
||||
let account = account_from_auth(&ctx, &req).await?;
|
||||
|
||||
if !(account.perms().owner() || account.perms().board_banners()) {
|
||||
if !(account.perms().owner() || account.perms().banners()) {
|
||||
return Err(NekrochanError::InsufficientPermissionError);
|
||||
}
|
||||
|
||||
let board = board.into_inner();
|
||||
let board = Board::read(&ctx, board.clone())
|
||||
.await?
|
||||
.ok_or(NekrochanError::BoardNotFound(board))?;
|
||||
|
||||
let template = BannersTemplate { tcx, board };
|
||||
let banners = Banner::read_all(&ctx).await?;
|
||||
let template = BannersTemplate { tcx, banners };
|
||||
|
||||
template_response(&template)
|
||||
}
|
||||
|
@ -24,10 +24,7 @@ pub async fn boards(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, Ne
|
||||
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
||||
let account = account_from_auth(&ctx, &req).await?;
|
||||
|
||||
if !(account.perms().owner()
|
||||
|| account.perms().board_config()
|
||||
|| account.perms().board_banners())
|
||||
{
|
||||
if !(account.perms().owner() || account.perms().board_config() || account.perms().banners()) {
|
||||
return Err(NekrochanError::InsufficientPermissionError);
|
||||
}
|
||||
|
||||
|
binární
static/banner.gif
binární
static/banner.gif
Binární soubor nebyl zobrazen.
Před Šířka: | Výška: | Velikost: 206 KiB |
binární
static/default-banner.png
Normální soubor
binární
static/default-banner.png
Normální soubor
Binární soubor nebyl zobrazen.
Za Šířka: | Výška: | Velikost: 19 KiB |
@ -8,9 +8,7 @@
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="center">
|
||||
{% if let Some(banner) = board.random_banner() %}
|
||||
<img class="banner" src="{{ banner.file_url() }}">
|
||||
{% endif %}
|
||||
<img class="banner" src="/random-banner">
|
||||
<h1 class="title">Katalog (<a href="/boards/{{ board.id }}">/{{ board.id }}/</a>)</h1>
|
||||
<p class="description">{{ board.description }}</p>
|
||||
<a href="/boards/{{ board.id }}">Index</a>
|
||||
|
@ -10,9 +10,7 @@
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="center">
|
||||
{% if let Some(banner) = board.random_banner() %}
|
||||
<img class="banner" src="{{ banner.file_url() }}">
|
||||
{% endif %}
|
||||
<img class="banner" src="/random-banner">
|
||||
<h1 class="title">/{{ board.id }}/ - {{ board.name }}</h1>
|
||||
<p class="description">{{ board.description }}</p>
|
||||
<a href="/boards/{{ board.id }}/catalog">Katalog</a>
|
||||
|
@ -5,9 +5,6 @@
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="center">
|
||||
{% if let Some(banner) = tcx.cfg.site.site_banner %}
|
||||
<img class="banner" src="{{ banner }}">
|
||||
{% endif %}
|
||||
<h1 class="title">{{ tcx.cfg.site.name }}</h1>
|
||||
<p class="description">{{ tcx.cfg.site.description }}</p>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<a href="/staff/account">[Účet]</a> 
|
||||
<a href="/staff/accounts">[Účty]</a> 
|
||||
|
||||
{% if perms.owner() || perms.board_config() || perms.board_banners() %}
|
||||
{% if perms.owner() || perms.board_config() || perms.banners() %}
|
||||
<a href="/staff/boards">[Nástěnky]</a> 
|
||||
{% endif %}
|
||||
|
||||
@ -11,6 +11,10 @@
|
||||
<a href="/staff/bans">[Bany]</a> 
|
||||
{% endif %}
|
||||
|
||||
{% if perms.owner() || perms.banners() %}
|
||||
<a href="/staff/banners">[Bannery]</a> 
|
||||
{% endif %}
|
||||
|
||||
{% if perms.owner() || perms.reports() %}
|
||||
<a href="/staff/reports">[Hlášení]</a> 
|
||||
{% endif %}
|
||||
|
@ -8,9 +8,7 @@
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="center">
|
||||
{% if let Some(banner) = tcx.cfg.site.site_banner %}
|
||||
<img class="banner" src="{{ banner }}">
|
||||
{% endif %}
|
||||
<img class="banner" src="/random-banner">
|
||||
<h1 class="title">Katalog nadnástěnky</h1>
|
||||
<p class="description">Nově naťuknutá vlákna ze všech nástěnek</p>
|
||||
<a href="/overboard">Index</a>
|
||||
|
@ -9,9 +9,7 @@
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="center">
|
||||
{% if let Some(banner) = tcx.cfg.site.site_banner %}
|
||||
<img class="banner" src="{{ banner }}">
|
||||
{% endif %}
|
||||
<img class="banner" src="/random-banner">
|
||||
<h1 class="title">Index nadnástěnky</h1>
|
||||
<p class="description">Nově naťuknutá vlákna ze všech nástěnek</p>
|
||||
<a href="/overboard/catalog">Katalog</a>
|
||||
|
@ -2,27 +2,25 @@
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Bannery (/{{ board.id }}/){% endblock %}
|
||||
{% block title %}Bannery{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">Bannery (/{{ board.id }}/)</h1>
|
||||
<h1 class="title">Bannery</h1>
|
||||
{% call staff_nav::staff_nav(tcx.perms) %}
|
||||
<hr>
|
||||
<h2>Bannery</h2>
|
||||
<form method="post" action="/staff/actions/remove-banners">
|
||||
<input name="board" type="hidden" value="{{ board.id }}">
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Banner</th>
|
||||
</tr>
|
||||
{% for banner in board.banners.0 %}
|
||||
{% for banner in banners %}
|
||||
<tr>
|
||||
<td><input name="banners[]" type="checkbox" value="{{ loop.index0 }}"></td>
|
||||
<td><input name="banners[]" type="checkbox" value="{{ banner.id }}"></td>
|
||||
<td>
|
||||
<img class="banner" src="{{ banner.file_url() }}">
|
||||
<img class="banner" src="{{ banner.banner.file_url() }}">
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -34,8 +32,6 @@
|
||||
<hr>
|
||||
<h2>Přidat bannery</h2>
|
||||
<form method="post" enctype="multipart/form-data" action="/staff/actions/add-banners">
|
||||
<input name="board" type="hidden" value="{{ board.id }}">
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<td class="label">Bannery</td>
|
||||
|
@ -18,7 +18,6 @@
|
||||
<th>Jméno</th>
|
||||
<th>Popis</th>
|
||||
<th>Vytvořena</th>
|
||||
<th>Bannery</th>
|
||||
<th>Nastavení</th>
|
||||
</tr>
|
||||
{% for board in boards %}
|
||||
@ -28,7 +27,6 @@
|
||||
<td>{{ board.name }}</td>
|
||||
<td>{{ board.description }}</td>
|
||||
<td>{{ board.created|czech_datetime }}</td>
|
||||
<td>{% if tcx.perms.owner() || tcx.perms.board_banners() %}<a href="/staff/banners/{{ board.id }}">[Zobrazit]</a>{% else %}-{% endif %}</td>
|
||||
<td>{% if tcx.perms.owner() || tcx.perms.board_config() %}<a href="/staff/board-config/{{ board.id }}">[Zobrazit]</a>{% else %}-{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -71,10 +71,10 @@
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="label">Bannery nástěnek</td>
|
||||
<td class="label">Bannery</td>
|
||||
<td>
|
||||
<div class="input-wrapper">
|
||||
<input name="board_banners" type="checkbox"{% if account.perms().board_banners() %} checked="checked"{% endif %}{% if !tcx.perms.owner() %} disabled=""{% endif %}>
|
||||
<input name="banners" type="checkbox"{% if account.perms().banners() %} checked="checked"{% endif %}{% if !tcx.perms.owner() %} disabled=""{% endif %}>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -9,9 +9,7 @@
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="center">
|
||||
{% if let Some(banner) = board.random_banner() %}
|
||||
<img class="banner" src="{{ banner.file_url() }}">
|
||||
{% endif %}
|
||||
<img class="banner" src="/random-banner">
|
||||
<h1 class="title">/{{ board.id }}/ - {{ board.name }}</h1>
|
||||
<p class="description">{{ board.description }}</p>
|
||||
<a href="/boards/{{ board.id }}/catalog">Katalog</a>
|
||||
|
Načítá se…
Odkázat v novém úkolu
Zablokovat Uživatele