mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core: blog (draft)
This commit is contained in:
1 parent
d40f56e676
commit
e935558702
17 files changed
+440
-18
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "3.2.21",
|
||||
"version": "3.2.22",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/loader",
|
||||
"module": "src/loader",
|
||||
|
||||
@@ -20,13 +20,13 @@ export const builtinModel = [
|
||||
'builtin', 'document', 'domain', 'blacklist', 'opcount',
|
||||
'setting', 'token', 'user', 'storage', 'problem',
|
||||
'record', 'contest', 'message', 'solution', 'training',
|
||||
'discussion', 'system', 'oplog',
|
||||
'discussion', 'system', 'oplog', 'blog',
|
||||
];
|
||||
|
||||
export const builtinHandler = [
|
||||
'home', 'problem', 'record', 'judge', 'user',
|
||||
'contest', 'training', 'discussion', 'manage', 'import',
|
||||
'misc', 'homework', 'domain', 'status', 'api',
|
||||
'misc', 'homework', 'domain', 'status', 'api', 'blog',
|
||||
];
|
||||
|
||||
export const builtinScript = [
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { DiscussionNotFoundError } from '../error';
|
||||
import { BlogDoc } from '../interface';
|
||||
import paginate from '../lib/paginate';
|
||||
import * as blog from '../model/blog';
|
||||
import { PERM, PRIV } from '../model/builtin';
|
||||
import * as discussion from '../model/discussion';
|
||||
import * as oplog from '../model/oplog';
|
||||
import user from '../model/user';
|
||||
import {
|
||||
Handler, param, Route, Types,
|
||||
} from '../service/server';
|
||||
|
||||
class BlogHandler extends Handler {
|
||||
ddoc?: BlogDoc;
|
||||
|
||||
@param('did', Types.ObjectID, true)
|
||||
async _prepare(domainId: string, did: ObjectID) {
|
||||
this.checkPerm(PERM.PERM_VIEW_DISCUSSION);
|
||||
if (did) {
|
||||
this.ddoc = await blog.get(did);
|
||||
if (!this.ddoc) throw new DiscussionNotFoundError(domainId, did);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BlogUserHandler extends BlogHandler {
|
||||
@param('uid', Types.Int)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, uid: number, page = 1) {
|
||||
const [ddocs, dpcount] = await paginate(
|
||||
blog.getMulti({ owner: uid }),
|
||||
page,
|
||||
10,
|
||||
);
|
||||
const udoc = await user.getById(domainId, uid);
|
||||
this.response.template = 'blog_main.html';
|
||||
this.response.body = {
|
||||
ddocs,
|
||||
dpcount,
|
||||
udoc,
|
||||
page,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class BlogDetailHandler extends BlogHandler {
|
||||
@param('did', Types.ObjectID)
|
||||
async get(domainId: string, did: ObjectID) {
|
||||
const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE)
|
||||
? await blog.getStatus(did, this.user._id)
|
||||
: null;
|
||||
const udoc = await user.getById(domainId, this.ddoc.owner);
|
||||
if (!dsdoc?.view) {
|
||||
await Promise.all([
|
||||
blog.inc(did, 'views', 1),
|
||||
blog.setStatus(did, this.user._id, { view: true }),
|
||||
]);
|
||||
}
|
||||
this.response.template = 'blog_detail.html';
|
||||
this.response.body = {
|
||||
ddoc: this.ddoc, dsdoc, udoc,
|
||||
};
|
||||
}
|
||||
|
||||
async post() {
|
||||
this.checkPriv(PRIV.PRIV_USER_PROFILE);
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectID)
|
||||
async postStar(domainId: string, did: ObjectID) {
|
||||
await blog.setStar(did, this.user._id, true);
|
||||
this.back({ star: true });
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectID)
|
||||
async postUnstar(domainId: string, did: ObjectID) {
|
||||
await blog.setStar(did, this.user._id, false);
|
||||
this.back({ star: false });
|
||||
}
|
||||
}
|
||||
|
||||
class BlogEditHandler extends BlogHandler {
|
||||
async get() {
|
||||
this.response.template = 'blog_edit.html';
|
||||
this.response.body = { ddoc: this.ddoc };
|
||||
}
|
||||
|
||||
@param('title', Types.Title)
|
||||
@param('content', Types.Content)
|
||||
async postCreate(domainId: string, title: string, content: string) {
|
||||
this.checkPerm(PERM.PERM_CREATE_DISCUSSION);
|
||||
await this.limitRate('add_blog', 3600, 60);
|
||||
const did = await blog.add(this.user._id, title, content, this.request.ip);
|
||||
this.response.body = { did };
|
||||
this.response.redirect = this.url('blog_detail', { uid: this.user._id, did });
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectID)
|
||||
@param('title', Types.Title)
|
||||
@param('content', Types.Content)
|
||||
async postUpdate(domainId: string, did: ObjectID, title: string, content: string) {
|
||||
if (!this.user.own(this.ddoc)) this.checkPerm(PERM.PERM_EDIT_DISCUSSION);
|
||||
else this.checkPerm(PERM.PERM_EDIT_DISCUSSION_SELF);
|
||||
await blog.edit(did, title, content);
|
||||
await oplog.add({
|
||||
...this.ddoc, operator: this.user._id, type: 'edit',
|
||||
});
|
||||
this.response.body = { did };
|
||||
this.response.redirect = this.url('blog_detail', { uid: this.user._id, did });
|
||||
}
|
||||
|
||||
@param('did', Types.ObjectID)
|
||||
async postDelete(domainId: string, did: ObjectID) {
|
||||
if (!this.user.own(this.ddoc)) this.checkPerm(PERM.PERM_DELETE_DISCUSSION);
|
||||
else this.checkPerm(PERM.PERM_DELETE_DISCUSSION_SELF);
|
||||
await discussion.del(domainId, did);
|
||||
await oplog.add({
|
||||
...this.ddoc, operator: this.user._id, operateIp: this.request.ip, type: 'delete',
|
||||
});
|
||||
this.response.redirect = this.url('blog_main', { uid: this.ddoc.owner });
|
||||
}
|
||||
}
|
||||
|
||||
export async function apply() {
|
||||
Route('blog_main', '/blog/:uid', BlogUserHandler);
|
||||
Route('blog_create', '/blog/:uid/create', BlogEditHandler);
|
||||
Route('blog_detail', '/blog/:uid/:did', BlogDetailHandler);
|
||||
Route('blog_edit', '/blog/:uid/:did/edit', BlogEditHandler);
|
||||
}
|
||||
|
||||
global.Hydro.handler.blog = apply;
|
||||
@@ -206,7 +206,7 @@ class HomeworkScoreboardDownloadHandler extends Handler {
|
||||
csv: (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`,
|
||||
html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }),
|
||||
};
|
||||
if (!getContent[ext]) throw new ValidationError('ext');
|
||||
if (!getContent[ext]) throw new ValidationError('ext', null, 'Unknown file extension');
|
||||
const [tdoc, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0);
|
||||
this.binary(await getContent[ext](rows), `${tdoc.title}.${ext}`);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,13 @@ export class FilesHandler extends Handler {
|
||||
this.back();
|
||||
}
|
||||
|
||||
@post('from', Types.String)
|
||||
@post('to', Types.String)
|
||||
async postRename(domainId: string, from: string, to: string) {
|
||||
await storage.rename(`user/${this.user._id}/${from}`, `user/${this.user._id}/${to}`);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@post('files', Types.Array)
|
||||
async postDeleteFiles(domainId: string, files: string[]) {
|
||||
await Promise.all([
|
||||
|
||||
@@ -189,7 +189,7 @@ class TrainingCreateHandler extends Handler {
|
||||
if (pin) this.checkPerm(PERM.PERM_PIN_TRAINING);
|
||||
const dag = await _parseDagJson(domainId, _dag);
|
||||
const pids = training.getPids(dag);
|
||||
assert(pids.length, new ValidationError('dag'));
|
||||
assert(pids.length, new ValidationError('dag', null, 'Please specify at least one problem'));
|
||||
const pdocs = await problem.getMulti(domainId, { docId: { $in: pids } }).sort('_id', 1).toArray();
|
||||
for (const pdoc of pdocs) {
|
||||
if (pdoc.hidden) this.checkPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
||||
|
||||
@@ -436,6 +436,20 @@ export interface DiscussionTailReplyDoc {
|
||||
history: HistoryDoc[],
|
||||
}
|
||||
|
||||
export interface BlogDoc {
|
||||
docType: document['TYPE_BLOG'];
|
||||
docId: ObjectID;
|
||||
owner: number;
|
||||
title: string;
|
||||
content: string;
|
||||
ip: string;
|
||||
updateAt: Date;
|
||||
nReply: number;
|
||||
views: number;
|
||||
reply: any[];
|
||||
react: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface TokenDoc {
|
||||
_id: string,
|
||||
tokenType: number,
|
||||
@@ -587,6 +601,7 @@ export interface Collections {
|
||||
|
||||
export interface Model {
|
||||
blacklist: typeof import('./model/blacklist').default,
|
||||
blog: typeof import('./model/blog'),
|
||||
builtin: typeof import('./model/builtin'),
|
||||
contest: typeof import('./model/contest'),
|
||||
discussion: typeof import('./model/discussion'),
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { omit } from 'lodash';
|
||||
import { FilterQuery, ObjectID } from 'mongodb';
|
||||
import { BlogDoc } from '../interface';
|
||||
import * as bus from '../service/bus';
|
||||
import { NumberKeys } from '../typeutils';
|
||||
import * as document from './document';
|
||||
|
||||
export async function add(
|
||||
owner: number, title: string, content: string,
|
||||
ip: string | null = null,
|
||||
): Promise<ObjectID> {
|
||||
const payload: Partial<BlogDoc> = {
|
||||
content,
|
||||
owner,
|
||||
title,
|
||||
ip,
|
||||
nReply: 0,
|
||||
updateAt: new Date(),
|
||||
views: 0,
|
||||
};
|
||||
await bus.serial('blog/before-add', payload);
|
||||
const res = await document.add(
|
||||
'system', payload.content!, payload.owner!, document.TYPE_BLOG,
|
||||
null, null, null, omit(payload, ['domainId', 'content', 'owner']),
|
||||
);
|
||||
payload.docId = res;
|
||||
await bus.emit('blog/add', payload);
|
||||
return payload.docId;
|
||||
}
|
||||
|
||||
export async function get(did: ObjectID): Promise<BlogDoc> {
|
||||
return await document.get('system', document.TYPE_BLOG, did);
|
||||
}
|
||||
|
||||
export function edit(did: ObjectID, title: string, content: string): Promise<BlogDoc> {
|
||||
const payload = { title, content };
|
||||
return document.set('system', document.TYPE_BLOG, did, payload);
|
||||
}
|
||||
|
||||
export function inc(did: ObjectID, key: NumberKeys<BlogDoc>, value: number): Promise<BlogDoc | null> {
|
||||
return document.inc('system', document.TYPE_BLOG, did, key, value);
|
||||
}
|
||||
|
||||
export function del(did: ObjectID): Promise<never> {
|
||||
return Promise.all([
|
||||
document.deleteOne('system', document.TYPE_BLOG, did),
|
||||
document.deleteMultiStatus('system', document.TYPE_BLOG, { docId: did }),
|
||||
]) as any;
|
||||
}
|
||||
|
||||
export function count(query: FilterQuery<BlogDoc>) {
|
||||
return document.count('system', document.TYPE_BLOG, query);
|
||||
}
|
||||
|
||||
export function getMulti(query: FilterQuery<BlogDoc> = {}) {
|
||||
return document.getMulti('system', document.TYPE_BLOG, query)
|
||||
.sort({ _id: -1 });
|
||||
}
|
||||
|
||||
export async function addReply(did: ObjectID, owner: number, content: string, ip: string): Promise<ObjectID> {
|
||||
const [[, drid]] = await Promise.all([
|
||||
document.push('system', document.TYPE_BLOG, did, 'reply', content, owner, { ip }),
|
||||
document.incAndSet('system', document.TYPE_BLOG, did, 'nReply', 1, { updateAt: new Date() }),
|
||||
]);
|
||||
return drid;
|
||||
}
|
||||
|
||||
export function setStar(did: ObjectID, uid: number, star: boolean) {
|
||||
return document.setStatus('system', document.TYPE_BLOG, did, uid, { star });
|
||||
}
|
||||
|
||||
export function getStatus(did: ObjectID, uid: number) {
|
||||
return document.getStatus('system', document.TYPE_BLOG, did, uid);
|
||||
}
|
||||
|
||||
export function setStatus(did: ObjectID, uid: number, $set) {
|
||||
return document.setStatus('system', document.TYPE_BLOG, did, uid, $set);
|
||||
}
|
||||
|
||||
global.Hydro.model.blog = {
|
||||
add,
|
||||
get,
|
||||
inc,
|
||||
edit,
|
||||
del,
|
||||
count,
|
||||
getMulti,
|
||||
addReply,
|
||||
setStar,
|
||||
getStatus,
|
||||
setStatus,
|
||||
};
|
||||
@@ -264,8 +264,8 @@ export async function getVnode(domainId: string, type: number, id: string, uid?:
|
||||
return { ...tdoc, type, id: _id };
|
||||
}
|
||||
return {
|
||||
...await getNode(domainId, id),
|
||||
title: id,
|
||||
...await getNode(domainId, id),
|
||||
type,
|
||||
id,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
Cursor, FilterQuery, ObjectID, OnlyFieldsOfType, UpdateQuery,
|
||||
} from 'mongodb';
|
||||
import {
|
||||
Content, DiscussionDoc, DiscussionReplyDoc, ProblemDoc, ProblemStatusDoc, Tdoc, TrainingDoc,
|
||||
BlogDoc, Content, DiscussionDoc,
|
||||
DiscussionReplyDoc, ProblemDoc, ProblemStatusDoc,
|
||||
Tdoc, TrainingDoc,
|
||||
} from '../interface';
|
||||
import * as bus from '../service/bus';
|
||||
import db from '../service/db';
|
||||
@@ -25,16 +27,18 @@ export const TYPE_DISCUSSION_REPLY: 22 = 22;
|
||||
export const TYPE_CONTEST: 30 = 30;
|
||||
export const TYPE_TRAINING: 40 = 40;
|
||||
export const TYPE_HOMEWORK: 60 = 60;
|
||||
export const TYPE_BLOG: 70 = 70;
|
||||
|
||||
export interface DocType {
|
||||
[TYPE_PROBLEM]: ProblemDoc,
|
||||
[TYPE_PROBLEM_SOLUTION]: any,
|
||||
[TYPE_PROBLEM_LIST]: any,
|
||||
[TYPE_DISCUSSION_NODE]: any,
|
||||
[TYPE_DISCUSSION]: DiscussionDoc,
|
||||
[TYPE_DISCUSSION_REPLY]: DiscussionReplyDoc,
|
||||
[TYPE_CONTEST]: Tdoc,
|
||||
[TYPE_TRAINING]: TrainingDoc,
|
||||
[TYPE_PROBLEM]: ProblemDoc;
|
||||
[TYPE_PROBLEM_SOLUTION]: any;
|
||||
[TYPE_PROBLEM_LIST]: any;
|
||||
[TYPE_DISCUSSION_NODE]: any;
|
||||
[TYPE_DISCUSSION]: DiscussionDoc;
|
||||
[TYPE_DISCUSSION_REPLY]: DiscussionReplyDoc;
|
||||
[TYPE_CONTEST]: Tdoc;
|
||||
[TYPE_TRAINING]: TrainingDoc;
|
||||
[TYPE_BLOG]: BlogDoc;
|
||||
}
|
||||
|
||||
export interface DocStatusType {
|
||||
@@ -479,4 +483,5 @@ global.Hydro.model.document = {
|
||||
TYPE_PROBLEM_LIST,
|
||||
TYPE_PROBLEM_SOLUTION,
|
||||
TYPE_TRAINING,
|
||||
TYPE_BLOG,
|
||||
};
|
||||
@@ -30,7 +30,7 @@ class RecordModel {
|
||||
const pending = await task.count({ uid });
|
||||
const timeRecent = await RecordModel.coll
|
||||
.find({ _id: { $gte: Time.getObjectID(moment().add(-1, 'hour')) }, uid }).project({ time: 1 }).toArray();
|
||||
return base - ((pending * 100 + 1) * (sum(timeRecent.map((i) => i.time || 0)) / 10000));
|
||||
return base - ((pending * 1000 + 1) * (sum(timeRecent.map((i) => i.time || 0)) / 10000));
|
||||
}
|
||||
|
||||
static async get(_id: ObjectID): Promise<RecordDoc | null>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.27.9",
|
||||
"version": "4.27.10",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "hydro.js",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.page--discussion_detail
|
||||
.page--discussion_detail,
|
||||
.page--blog_detail
|
||||
|
||||
.topic__content
|
||||
margin: rem(20px 0)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.page--discussion_main,
|
||||
.page--discussion_node,
|
||||
.page--blog_main,
|
||||
.page--user_detail
|
||||
.discussion__item
|
||||
border-left: rem(5px) solid transparent
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<div class="section__body">
|
||||
<ul class="supplementary list">
|
||||
<li>
|
||||
<a class="discussion-node-tag" href="{{ url('blog_main', uid=udoc._id) }}">
|
||||
<span class="v-center icon icon-flag"></span>
|
||||
{{ _("{0}'s blog").format(udoc.uname) }}
|
||||
</a>
|
||||
</li>
|
||||
<li><h2>{{ ddoc.title }}</h2></li>
|
||||
<li>
|
||||
{{ user.render_inline(udict[ddoc.owner]) }}
|
||||
@
|
||||
{{ datetimeSpan(ddoc._id)|safe }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="typo topic__content" data-emoji-enabled>
|
||||
{{ ddoc['content']|markdown|safe }}
|
||||
</div>
|
||||
</div>
|
||||
<ul class="section__footer supplementary dot list">
|
||||
{% if handler.user.hasPerm(perm.PERM_EDIT_DISCUSSION) or (handler.user.own(ddoc) and handler.user.hasPerm(perm.PERM_EDIT_DISCUSSION_SELF)) %}
|
||||
<li><a href="{{ url('blog_edit', uid=udoc._id, did=ddoc.docId) }}">
|
||||
<span class="icon icon-edit"></span> {{ _('Edit') }}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
<li>{{ _('{0} views').format(ddoc.views) }}</li>
|
||||
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
|
||||
<li><form class="form--inline" method="post">
|
||||
<input type="hidden" name="operation" value="{% if not dsdoc['star'] %}star{% else %}unstar{% endif %}">
|
||||
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
||||
<button class="star{% if dsdoc['star'] %} activated{% endif %}" type="submit">
|
||||
<span class="starred--hide"><span class="icon icon-star--outline"></span> {{ _('Star Topic') }}</span>
|
||||
<span class="starred--show"><span class="icon icon-star"></span> {{ _('Unstar Topic') }}</span>
|
||||
</button>
|
||||
</form></li>
|
||||
{% endif %}
|
||||
<li><a href="{{ url('wiki_help', anchor='contact') }}">
|
||||
<span class="icon icon-warning"></span> {{ _('Report') }}
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
<div class="section side">
|
||||
<div class="profile__bg user-profile-bg--1"></div>
|
||||
<div class="section__body">
|
||||
<div class="media sidebar-user-profile">
|
||||
<div class="media__left">
|
||||
<img src="{{ avatarUrl(udoc.avatar, 80) }}" width="80" height="80" class="medium user-profile-avatar">
|
||||
</div>
|
||||
<div class="media__body">
|
||||
<p>
|
||||
{{ user.render_inline(udoc, avatar=false, modbadge=false) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,52 @@
|
||||
{% import "components/form.html" as form with context %}
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<div class="section__body">
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
<div class="medium-12 columns">
|
||||
<label>
|
||||
{{ _('Title') }}
|
||||
<input
|
||||
name="title" placeholder="{{ _('title') }}"
|
||||
value="{{ ddoc.title|default('') }}"
|
||||
class="textbox" autofocus>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{ form.form_textarea({
|
||||
columns:12,
|
||||
label:'Content',
|
||||
name:'content',
|
||||
value:ddoc.content|default(''),
|
||||
hotkeys:'ctrl+enter:submit',
|
||||
markdown:true,
|
||||
required:true,
|
||||
extra_style:'height: 500px',
|
||||
extra_textarea_class:'auto-resize'
|
||||
}) }}
|
||||
<div class="row"><div class="columns">
|
||||
<button name="operation" value="{{ 'update' if ddoc else 'create' }}" type="submit" class="rounded primary button">
|
||||
{{ _('Update') if ddoc else _('Create') }} (Ctrl+Enter)
|
||||
</button>
|
||||
{% if ddoc and (handler.user.own(ddoc) or handler.user.hasPerm(perm.PERM_DELETE_DISCUSSION)) %}
|
||||
<button name="operation" value="delete" type="submit" class="rounded button">
|
||||
{{ _('Delete') }}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="rounded button" onclick="window.history.go(-1)">
|
||||
{{ _('Cancel') }}
|
||||
</button>
|
||||
</div></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
{% include 'components/md_hint.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,51 @@
|
||||
{% import "components/nothing.html" as nothing with context %}
|
||||
{% import "components/user.html" as user with context %}
|
||||
{% import "components/paginator.html" as paginator with context %}
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
{% if not ddocs.length %}
|
||||
{{ nothing.render('no blog yet...') }}
|
||||
{% else %}
|
||||
<ol class="section__list discussion__list">
|
||||
{%- for ddoc in ddocs -%}
|
||||
<li class="section__list__item discussion__item">
|
||||
<div class="media">
|
||||
<div class="media__left">
|
||||
<div class="discussion__replies numbox">
|
||||
<div class="numbox__num large">{{ ddoc.nReply }}</div>
|
||||
<div class="numbox__text">{{ _('Comments') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media__body top">
|
||||
<h1 class="discussion__title"><a href="{{ url('blog_detail', uid=udoc._id, did=ddoc._id, anchor=ddoc.updateAt.getTime()) }}" data-emoji-enabled>{{ ddoc['title'] }}</a></h1>
|
||||
<ul class="supplementary list">
|
||||
<li>{{ _('{0} views').format(ddoc.views) }}</li>
|
||||
<li>{{ datetimeSpan(ddoc.updateAt)|safe }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{%- endfor -%}
|
||||
</ol>
|
||||
{% if page and dpcount %}{{ paginator.render(page, dpcount) }}{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
<div class="section side">
|
||||
{% if udoc.blogpic %}<div class="discussion-node__bg {{ udoc.blogpic }}"></div>{% endif %}
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _("{0}'s blog").format(udoc.uname) }}</h1>
|
||||
</div>
|
||||
<div class="section__body">
|
||||
{% if handler.user.hasPerm(perm.PERM_CREATE_DISCUSSION) and handler.user._id == udoc._id %}
|
||||
<p><a href="{{ url('blog_create', uid=udoc._id) }}" class="expanded primary button">{{ _('Create a Post') }}</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user