client/users: add user view prototype

This commit is contained in:
rr- 2016-04-06 22:34:21 +02:00
parent c46dc08c1b
commit 8be93f6c70
14 changed files with 225 additions and 21 deletions

View File

@ -83,6 +83,13 @@ nav ul li img {
vertical-align: top; /* fix ghost margin under the image */
}
nav.plain-nav ul li {
display: block;
}
nav.plain-nav ul li a {
display: inline;
}
nav.text-nav {
margin: 1em 0;
}

View File

@ -36,3 +36,27 @@
#login .buttons a {
margin-left: 1em;
}
#user {
width: 30em;
}
#user .text-nav {
margin-bottom: 1.5em;
}
#user-summary img {
width: 6em;
margin: 0 1.5em 1.5em 0;
float: left;
}
#user-summary div {
clear: both;
}
#user-summary .basic-info {
list-style-type: none;
margin: 0;
}
#user-summary nav {
float: left;
width: 45%;
margin-right: 1em;
}

View File

@ -1,5 +1,5 @@
<div class='content-wrapper transparent' id='home'>
<div class='messages'></div>
<h1>{{name}}</h1>
<footer>Version: <span class='version'>{{version}}</span> (built {{#reltime}}{{buildDate}}{{/reltime}})</footer>
<footer>Version: <span class='version'>{{version}}</span> (built {{reltime buildDate}})</footer>
</div>

12
client/html/user.hbs Normal file
View File

@ -0,0 +1,12 @@
<div class='messages'></div>
<div class='content-wrapper' id='user'>
<h1>{{this.name}}</h1>
<nav class='text-nav'><!--
--><ul><!--
--><li data-name='summary'><a href='/user/{{this.name}}'>Summary</a></li><!--
--><li data-name='edit'><a href='/user/{{this.name}}/edit'>Account settings</a></li><!--
--></ul><!--
--></nav>
<div id='user-content-holder'></div>
</div>

View File

@ -0,0 +1,3 @@
<div id='user-edit'>
<strong>Placeholder for account settings form</strong>
</div>

View File

@ -0,0 +1,29 @@
<div id='user-summary'>
<img src='{{this.user.avatarUrl}}' alt='{{this.user.name}}&rsquo;s avatar'/>
<ul class='basic-info'>
<li>Registered: {{reltime this.user.creationTime}}</li>
<li>Last seen: {{reltime this.user.lastLoginTime}}</li>
<li>Rank: {{toLowerCase this.user.rankName}}</li>
</ul>
<div>
<nav class='plain-nav'>
<p><strong>Quick links</strong></p>
<ul>
<li><a href='/posts/submit:{{this.user.name}}'>Uploads</a></li>
<li><a href='/posts/fav:{{this.user.name}}'>Favorites</a></li>
<li><a href='/posts/commented:{{this.user.name}}'>Posts commented on</a></li>
</ul>
</nav>
{{#if this.isPrivate}}
<nav class='plain-nav'>
<p><strong>Only visible to you</strong></p>
<ul>
<li><a href='/posts/special:liked'>Liked posts</a></li>
<li><a href='/posts/special:disliked'>Disliked posts</a></li>
</ul>
</nav>
{{/if}}
</div>
</div>

View File

@ -18,10 +18,13 @@ class UsersController {
page('/users', () => { this.listUsersRoute(); });
page(
'/user/:name',
(ctx, next) => { this.showUserRoute(ctx.params.name); });
(ctx, next) => { this.loadUserRoute(ctx, next); },
(ctx, next) => { this.showUserRoute(ctx, next); });
page(
'/user/:name/edit',
(ctx, next) => { this.editUserRoute(ctx.params.name); });
(ctx, next) => { this.loadUserRoute(ctx, next); },
(ctx, next) => { this.editUserRoute(ctx, next); });
page.exit('/user/', (ctx, next) => { this.user = null; });
}
listUsersRoute() {
@ -57,22 +60,42 @@ class UsersController {
});
}
showUserRoute(name) {
if (api.isLoggedIn() && name == api.userName) {
loadUserRoute(ctx, next) {
if (ctx.state.user) {
next();
} else if (this.user && this.user.name == ctx.params.name) {
ctx.state.user = this.user;
next();
} else {
api.get('/user/' + ctx.params.name).then(response => {
ctx.state.user = response.user;
ctx.save();
this.user = response.user;
next();
}).catch(response => {
this.userView.empty();
this.userView.notifyError(response.description);
});
}
}
_show(user, section) {
const isPrivate = api.isLoggedIn() && user.name == api.userName;
if (isPrivate) {
topNavController.activate('account');
} else {
topNavController.activate('users');
}
this.userView.empty();
api.get('/user/' + name).then(response => {
this.userView.render({user: response.user});
}).catch(response => {
this.userView.notifyError(response.description);
});
this.userView.render({
user: user, section: section, isPrivate: isPrivate});
}
editUserRoute(user) {
topNavController.activate('users');
showUserRoute(ctx, next) {
this._show(ctx.state.user, 'summary');
}
editUserRoute(ctx, next) {
this._show(ctx.state.user, 'edit');
}
}

View File

@ -3,13 +3,13 @@
const handlebars = require('handlebars');
const misc = require('./misc.js');
handlebars.registerHelper('reltime', function(options) {
handlebars.registerHelper('reltime', function(time) {
return new handlebars.SafeString(
'<time datetime="' +
options.fn(this) +
'" title="' +
options.fn(this) +
'">' +
misc.formatRelativeTime(options.fn(this)) +
'<time datetime="' + time + '" title="' + time + '">' +
misc.formatRelativeTime(time) +
'</time>');
});
handlebars.registerHelper('toLowerCase', function(str) {
return str.toLowerCase();
});

View File

@ -0,0 +1,16 @@
'use strict';
const BaseView = require('./base_view.js');
class UserEditView extends BaseView {
constructor() {
super();
this.template = this.getTemplate('user-edit-template');
}
render(options) {
options.target.innerHTML = this.template(options.user);
}
}
module.exports = UserEditView;

View File

@ -0,0 +1,17 @@
'use strict';
const BaseView = require('./base_view.js');
class UserSummaryView extends BaseView {
constructor() {
super();
this.template = this.getTemplate('user-summary-template');
}
render(options) {
options.target.innerHTML = this.template({
user: options.user, isPrivate: options.isPrivate});
}
}
module.exports = UserSummaryView;

View File

@ -0,0 +1,45 @@
'use strict';
const BaseView = require('./base_view.js');
const UserSummaryView = require('./user_summary_view.js');
const UserEditView = require('./user_edit_view.js');
class UserView extends BaseView {
constructor() {
super();
this.template = this.getTemplate('user-template');
this.summaryView = new UserSummaryView();
this.editView = new UserEditView();
}
render(options) {
let section = options.section;
if (!section) {
section = 'summary';
}
let view = null;
if (section == 'edit') {
view = this.editView;
} else {
view = this.summaryView;
}
this.showView(this.template(options.user));
options.target = this.contentHolder.querySelector(
'#user-content-holder');
view.render(options);
const allItemsSelector = '#content-holder [data-name]';
for (let item of document.querySelectorAll(allItemsSelector)) {
if (item.getAttribute('data-name') === section) {
item.className = 'active';
} else {
item.className = '';
}
}
}
}
module.exports = UserView;

View File

@ -7,6 +7,9 @@ secret: change
api_url: # where frontend connects to, example: http://api.example.com/
base_url: # used to form absolute links, example: http://example.com/
avatar_thumbnail_size: 200
post_thumbnail_size: 300
database:
schema: postgres
host: # example: localhost
@ -42,6 +45,13 @@ ranks:
- mod
- admin
- nobody
rank_names:
anonymous: 'Anonymous user'
regular_user: 'Regular user'
power_user: 'Power user'
mod: 'Moderator'
admin: 'Administrator'
nobody: 'God'
default_rank: regular_user
# don't change these, unless you want to annoy people. if you do customize

View File

@ -1,4 +1,5 @@
from szurubooru import errors, search
import hashlib
from szurubooru import config, errors, search
from szurubooru.util import auth, users
from szurubooru.api.base_api import BaseApi
@ -7,12 +8,23 @@ def _serialize_user(authenticated_user, user):
'id': user.user_id,
'name': user.name,
'rank': user.rank,
'rankName': config.config['rank_names'].get(user.rank, 'Unknown'),
'creationTime': user.creation_time,
'lastLoginTime': user.last_login_time,
'avatarStyle': user.avatar_style
}
if user.avatar_style == user.AVATAR_GRAVATAR:
md5 = hashlib.md5()
md5.update((user.email or user.name).lower().encode('utf-8'))
digest = md5.hexdigest()
ret['avatarUrl'] = 'http://gravatar.com/avatar/%s?s=%d' % (
digest, config.config['avatar_thumbnail_size'])
# TODO: else construct a link
if authenticated_user.user_id == user.user_id:
ret['email'] = user.email
return ret
class UserListApi(BaseApi):

View File

@ -13,7 +13,9 @@ class TestRetrievingUsers(DatabaseTestCase):
'users:view': 'regular_user',
'users:create': 'regular_user',
},
'avatar_thumbnail_size': 200,
'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
'rank_names': {},
})
util.mock_context(self)
@ -71,7 +73,9 @@ class TestCreatingUser(DatabaseTestCase):
'user_name_regex': '.{3,}',
'password_regex': '.{3,}',
'default_rank': 'regular_user',
'avatar_thumbnail_size': 200,
'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
'rank_names': {},
'privileges': {
'users:create': 'anonymous',
},
@ -130,7 +134,9 @@ class TestUpdatingUser(DatabaseTestCase):
'secret': '',
'user_name_regex': '.{3,}',
'password_regex': '.{3,}',
'avatar_thumbnail_size': 200,
'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
'rank_names': {},
'privileges': {
'users:edit:self:name': 'regular_user',
'users:edit:self:pass': 'regular_user',