user configurable profile wip

pull/83/head
Thibault Duplessis 2013-10-20 15:46:29 +02:00
parent 63fd5e3cbf
commit a9129ef30d
17 changed files with 229 additions and 150 deletions

View File

@ -0,0 +1,68 @@
package controllers
import play.api.mvc._, Results._
import lila.app._
import lila.common.LilaCookie
import lila.db.api.$find
import lila.security.Permission
import lila.user.tube.userTube
import lila.user.{ Context, User UserModel, UserRepo }
import views._
object Account extends LilaController {
private def env = Env.user
private def forms = lila.user.DataForm
def profile = Auth { implicit ctx
me
Ok(html.account.profile(me, forms.profile)).fuccess
}
def profileApply = AuthBody { implicit ctx
me
implicit val req = ctx.body
FormFuResult(forms.profile) { err
fuccess(html.account.profile(me, err))
} { profile
UserRepo.setProfile(me.id, profile) inject Redirect(routes.User show me.username)
}
}
def passwd = Auth { implicit ctx
me
Ok(html.account.passwd(me, forms.passwd)).fuccess
}
def passwdApply = AuthBody { implicit ctx
me
implicit val req = ctx.body
FormFuResult(forms.passwd) { err
fuccess(html.account.passwd(me, err))
} { passwd
for {
ok UserRepo.checkPassword(me.id, passwd.oldPasswd)
_ ok ?? UserRepo.passwd(me.id, passwd.newPasswd1)
} yield ok.fold(
Redirect(routes.User show me.username),
BadRequest(html.account.passwd(me, forms.passwd))
)
}
}
def close = Auth { implicit ctx
me
Ok(html.account.close(me)).fuccess
}
def closeConfirm = Auth { ctx
me
implicit val req = ctx.req
(UserRepo disable me.id) >>
Env.team.api.quitAll(me.id) >>
(Env.security disconnect me.id) inject {
Redirect(routes.User show me.username) withCookies LilaCookie.newSession
}
}
}

View File

@ -100,51 +100,6 @@ object User extends LilaController {
}
}
def setBio = AuthBody { ctx
me
implicit val req = ctx.body
forms.bio.bindFromRequest.fold(
f fulogwarn(f.errors.toString) inject ~me.bio,
bio UserRepo.setBio(me.id, bio) inject bio
) map { Ok(_) }
}
def passwd = Auth { implicit ctx
me
Ok(html.user.passwd(me, forms.passwd)).fuccess
}
def passwdApply = AuthBody { implicit ctx
me
implicit val req = ctx.body
FormFuResult(forms.passwd) { err
fuccess(html.user.passwd(me, err))
} { passwd
for {
ok UserRepo.checkPassword(me.id, passwd.oldPasswd)
_ ok ?? UserRepo.passwd(me.id, passwd.newPasswd1)
} yield ok.fold(
Redirect(routes.User show me.username),
BadRequest(html.user.passwd(me, forms.passwd))
)
}
}
def close = Auth { implicit ctx
me
Ok(html.user.close(me)).fuccess
}
def closeConfirm = Auth { ctx
me
implicit val req = ctx.req
(UserRepo disable me.id) >>
Env.team.api.quitAll(me.id) >>
(Env.security disconnect me.id) inject {
Redirect(routes.User show me.username) withCookies LilaCookie.newSession
}
}
def export(username: String) = Open { implicit ctx
OptionFuResult(UserRepo named username) { u
Env.game export u map { url

View File

@ -11,7 +11,7 @@ robots = false) {
<p class="explanation">
Are you sure you want to close your account? You will no longer be able to login!
</p>
<form action="@routes.User.closeConfirm" method="POST">
<form action="@routes.Account.closeConfirm" method="POST">
<br /><br />
<a href="@routes.User.show(u.username)">
I changed my mind, don't close my account

View File

@ -13,7 +13,7 @@ evenMoreCss = evenMoreCss) {
<div class="content_box small_box">
<div class="signup_box">
<h1 class="lichess_title">Change your password</h1>
<form action="@routes.User.passwdApply" method="POST">
<form action="@routes.Account.passwdApply" method="POST">
<ul>
@user.passwdFormField(form("oldPasswd"), "Current password")
@user.passwdFormField(form("newPasswd1"), "New password")

View File

@ -0,0 +1,31 @@
@(u: User, form: Form[_])(implicit ctx: Context)
@title = @{ "%s profile".format(u.username) }
@evenMoreCss = {
@cssTag("user-profile.css")
}
@user.layout(
title = title,
evenMoreCss = evenMoreCss) {
<div class="content_box small_box">
<div class="signup_box">
<h1 class="lichess_title">Edit your profile</h1>
<form action="@routes.Account.profileApply" method="POST">
<ul>
@user.passwdFormField(form("firstName"), "First name")
@user.passwdFormField(form("lastName"), "Last name")
@user.passwdFormField(form("bio"), "Biography")
@user.passwdFormField(form("country"), "Country")
<li>
@errMsg(form)
</li>
<li>
<input type="submit" class="submit" value="Apply changes" />
</li>
</ul>
</form>
</div>
</div>
}

View File

@ -7,33 +7,10 @@
@jsTagCompiled("chart2.js")
}
@evenMoreCss = {
@if(ctx is u) {
@cssTag("user-edit.css")
}
}
@bio = {
@if(ctx is u) {
<div class="user_bio">
<div class="editable">@shorten(u.bio | "Click here to tell about yourself", 400)</div>
<form action="@routes.User.setBio">
<textarea name="bio"></textarea>
<button class="button apply">@trans.apply()</button>
<button class="button cancel">@trans.cancel()</button>
</form>
</div>
} else {
@u.nonEmptyBio.map { bio =>
<div class="user_bio">@shorten(bio, 400)</div>
}
}
}
@actions = {
@if(ctx is u) {
<a class="small action" href="@routes.User.passwd">Change password</a>
<a class="small action" href="@routes.User.close">Close account</a>
<a class="small action" href="@routes.Account.passwd">Change password</a>
<a class="small action" href="@routes.Account.close">Close account</a>
}
}
@ -64,8 +41,7 @@
title = title,
goodies = goodies.some,
robots = false,
evenMoreJs = evenMoreJs,
evenMoreCss = evenMoreCss) {
evenMoreJs = evenMoreJs) {
<div class="content_box no_padding user_show">
<div class="content_box_top">
<div class="icon status @{isOnline(u.id).??("connected")}"></div>
@ -76,11 +52,11 @@ evenMoreCss = evenMoreCss) {
}
@if(u.disabled) {
<span class="staff">CLOSED</span>
@if(isGranted(_.ReopenAccount)) {
<form class="reopen" action="@routes.Mod.reopenAccount(u.username)" method="post">
<button type="submit" class="button confirm">Reopen</button>
</form>
}
@if(isGranted(_.ReopenAccount)) {
<form class="reopen" action="@routes.Mod.reopenAccount(u.username)" method="post">
<button type="submit" class="button confirm">Reopen</button>
</form>
}
}
</div>
<div class="social content_box_inter">
@ -118,7 +94,9 @@ evenMoreCss = evenMoreCss) {
@if(u.engine && ctx.me.fold(true)(u !=)) {
<div class="engine_warning">@trans.thisPlayerUsesChessComputerAssistance()</div>
}
@bio
@u.profileOrDefault.nonEmptyBio.map { bio =>
<div class="user_bio">@shorten(bio, 400)</div>
}
@info.confrontation.map { c =>
@user.confrontation(c)
}

View File

@ -0,0 +1,20 @@
var usersToMigrate = db.user2.find();
var collection = db.user3;
print("Migrating " + usersToMigrate.count() + " users");
collection.drop();
function nn(x) {
return (x | '') !== '';
}
usersToMigrate.forEach(function(u) {
if ((u.bio | '') !== '') u.profile = {bio: u.bio};
delete u.bio;
collection.insert(u);
});
print("Done!");

View File

@ -23,14 +23,18 @@ GET /@/:username/mod controllers.User.mod(username: String)
GET /@/:username/mini controllers.User.showMini(username: String)
GET /@/:username/:filterName controllers.User.showFilter(username: String, filterName: String, page: Int ?= 1)
GET /@/:username controllers.User.show(username: String)
GET /people controllers.User.list(page: Int ?= 1)
GET /people/online controllers.User.online
GET /people/autocomplete controllers.User.autocomplete
PUT /account/bio controllers.User.setBio
GET /account/passwd controllers.User.passwd
POST /account/passwd controllers.User.passwdApply
GET /account/close controllers.User.close
POST /account/closeConfirm controllers.User.closeConfirm
# Account
GET /account/passwd controllers.Account.passwd
POST /account/passwd controllers.Account.passwdApply
GET /account/close controllers.Account.close
POST /account/closeConfirm controllers.Account.closeConfirm
GET /account/profile controllers.Account.profile
POST /account/profile controllers.Account.profileApply
#Site
GET /socket controllers.Main.websocket

View File

@ -201,4 +201,6 @@ object Countries {
"zm" -> "Zambia",
"zw" -> "Zimbabwe",
"zz" -> "World")
val codeSet = all.keySet
}

View File

@ -9,6 +9,15 @@ object DataForm {
"bio" -> text(maxLength = 400)
))
val profile = Form(mapping(
"firstName" -> nameField,
"lastName" -> nameField,
"country" -> optional(nonEmptyText.verifying(Countries.codeSet contains _)),
"bio" -> optional(nonEmptyText(maxLength = 400))
)(Profile.apply)(Profile.unapply))
private def nameField = optional(nonEmptyText(minLength = 2, maxLength = 20))
val theme = Form(single(
"theme" -> nonEmptyText.verifying(Theme contains _)
))

View File

@ -0,0 +1,30 @@
package lila.user
import scala._
case class Profile(
firstName: Option[String] = None,
lastName: Option[String] = None,
bio: Option[String] = None,
country: Option[String] = None) {
def realName = (firstName |@| lastName) apply { _ + " " + _ }
def nonEmptyBio = bio filter (_.nonEmpty)
def nonEmpty = List(
firstName, lastName, bio, country
).flatten.nonEmpty option this
}
object Profile {
val default = Profile()
import lila.db.Tube
import play.api.libs.json._
private[user] lazy val tube = Tube[Profile](
Json.reads[Profile],
Json.writes[Profile])
}

View File

@ -16,8 +16,7 @@ case class User(
enabled: Boolean,
roles: List[String],
settings: Map[String, String] = Map.empty,
bio: Option[String] = None,
country: Option[String] = None,
profile: Option[Profile] = None,
engine: Boolean = false,
toints: Int = 0,
createdAt: DateTime,
@ -41,7 +40,7 @@ case class User(
def setting(name: String): Option[Any] = settings get name
def nonEmptyBio = bio filter ("" !=)
def profileOrDefault = profile | Profile.default
def hasGames = count.game > 0
@ -70,6 +69,7 @@ object User {
private implicit def countTube = Count.tube
private implicit def speedElosTube = SpeedElos.tube
private implicit def variantElosTube = VariantElos.tube
private implicit def profileTube = Profile.tube
private[user] lazy val tube = Tube[User](
(__.json update (

View File

@ -66,6 +66,14 @@ object UserRepo {
def setEloOnly(id: ID, elo: Int): Funit = $update($select(id), $set("elo" -> elo))
def setProfile(id: ID, profile: Profile): Funit = {
import tube.profileTube
profile.nonEmpty match {
case Some(p) => $update($select(id), $set("profile" -> p))
case None => $update($select(id), $unset("profile"))
}
}
val enabledSelect = Json.obj("enabled" -> true)
val noEngineSelect = Json.obj("engine" -> $ne(true))
val goodLadQuery = $query(enabledSelect ++ noEngineSelect)
@ -163,8 +171,6 @@ object UserRepo {
def setRoles(id: ID, roles: List[String]) = $update.field(id, "roles", roles)
def setBio(id: ID, bio: String) = $update.field(id, "bio", bio)
def setSpeedElos(id: ID, ses: SpeedElos) = {
import tube.speedElosTube
$update.field(id, "speedElos", ses)

View File

@ -11,6 +11,7 @@ package object user extends PackageObject with WithPlay {
private[user] implicit lazy val speedElosTube = SpeedElos.tube
private[user] implicit lazy val variantElosTube = VariantElos.tube
private[user] implicit lazy val profileTube = Profile.tube
private[user] implicit lazy val historyTube =
Tube.json inColl Env.current.historyColl

View File

@ -1,6 +1,8 @@
$(function() {
if($searchForm = $('form.search_user_form').orNot()) {
var $searchForm = $('form.search_user_form');
if($searchForm.length) {
$searchInput = $searchForm.find('input.search_user');
$searchInput.on('autocompleteselect', function(e, ui) {
setTimeout(function() {$searchForm.submit();},10);
@ -20,33 +22,6 @@ $(function() {
return false;
});
$('div.user_bio .editable').on('click', function() {
$editable = $(this);
$parent = $(this).parent().addClass('editing');
$form = $parent.find('form').show();
$form.find('textarea').val($editable.text());
function unedit() {
$form.find('button.cancel').off('click');
$form.off('submit');
$parent.removeClass('editing');
}
$form.find('button.cancel').on('click', function(e) {
unedit();
return false;
});
$form.on('submit', function() {
$.ajax({
url: $form.attr('action'),
type: 'PUT',
data: { bio: $form.find('textarea').val() },
success: function(t) {
$editable.text(t);
unedit();
}
});
return false;
});
});
});
function str_repeat(input, multiplier) {
return new Array(multiplier + 1).join(input);

View File

@ -1,32 +0,0 @@
div.user_bio .editable {
width: 290px;
padding: 5px;
border: 1px solid #88aaff;
cursor: pointer;
display: block;
margin-bottom: 1em;
}
div.user_bio .editable:hover {
border-color: blue;
}
div.user_bio form {
display: none;
margin-bottom: 1em;
}
div.user_bio textarea {
width: 285px;
padding: 5px;
height: 5em;
margin-bottom: 5px;
}
div.user_bio button {
margin-right: 5px;
}
div.user_bio.editing .editable {
display: none;
}
div.user_bio.editing form {
display: block;
}

View File

@ -0,0 +1,32 @@
.content_box form {
font-size: 1.3em;
}
.content_box form li {
display: block;
list-style: none outside none;
margin: 1em 0;
position: relative;
}
.content_box form label {
display: block;
float: left;
margin-right: 10px;
margin-top: 2px;
text-align: right;
width: 150px;
}
.content_box form input.passwd {
border: 1px solid #D4D4D4;
padding: 3px 5px;
width: 200px;
}
.content_box form input.submit {
margin-left: 160px;
margin-right: 20px;
padding: 3px 1em;
}
form .error {
color: red;
margin-left: 160px;
}