Implemented an extended user profile based on a patch from Jethro Carr (issue 510).

Changes with respect to the original patch:
- use Gconf instead of separate table / data scheme
- better form validation for URLs and emails
- no htmlentity-encoded contents in the database (pluf automatically safe-encodes
  stuff before it writes out contents into templates)
- add visual separators in the form views to have a distinct view of basic
  (important) data and other data which are only displayed in the public profile
- give a hint about the maximum display size of 60x60 px^2 and use max-width and
  max-height in the templates to avoid nasty distortions by the browser
- use target=_blank and rel=nofollow on the twitter and website links in the profile
- some whitespace / formatting / code style fixes
This commit is contained in:
Thomas Keller 2010-12-05 01:22:32 +01:00
parent 874b5aa7e9
commit 784c9718eb
18 changed files with 490 additions and 66 deletions

View File

@ -24,13 +24,15 @@
/** /**
* Update user's details. * Update user's details.
*/ */
class IDF_Form_Admin_UserUpdate extends Pluf_Form class IDF_Form_Admin_UserUpdate extends Pluf_Form
{ {
public $user = null; public $user = null;
public function initFields($extra=array()) public function initFields($extra=array())
{ {
$this->user = $extra['user']; $this->user = $extra['user'];
$user_data = IDF_UserData::factory($this->user);
$this->fields['first_name'] = new Pluf_Form_Field_Varchar( $this->fields['first_name'] = new Pluf_Form_Field_Varchar(
array('required' => false, array('required' => false,
'label' => __('First name'), 'label' => __('First name'),
@ -93,6 +95,66 @@ class IDF_Form_Admin_UserUpdate extends Pluf_Form
), ),
)); ));
$this->fields['description'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Description'),
'initial' => $user_data->description,
'widget_attrs' => array('rows' => 3,
'cols' => 40),
'widget' => 'Pluf_Form_Widget_TextareaInput',
));
$this->fields['twitter'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Twitter username'),
'initial' => $user_data->twitter,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['public_email'] = new Pluf_Form_Field_Email(
array('required' => false,
'label' => __('Public email address'),
'initial' => $user_data->public_email,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['website'] = new Pluf_Form_Field_Url(
array('required' => false,
'label' => __('Website URL'),
'initial' => $user_data->website,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['custom_avatar'] = new Pluf_Form_Field_File(
array('required' => false,
'label' => __('Upload custom avatar'),
'initial' => '',
'max_size' => Pluf::f('max_upload_size', 2097152),
'move_function_params' => array('upload_path' => Pluf::f('upload_path').'/avatars',
'upload_path_create' => true,
'upload_overwrite' => true,
'file_name' => 'user_'.$this->user->id.'_%s'),
'help_text' => __('An image file with a width and height not larger than 60 pixels (bigger images are scaled down).'),
));
$this->fields['remove_custom_avatar'] = new Pluf_Form_Field_Boolean(
array('required' => false,
'label' => __('Remove custom avatar'),
'initial' => false,
'widget' => 'Pluf_Form_Widget_CheckboxInput',
'widget_attrs' => array(),
'help_text' => __('Tick this to delete the custom avatar.'),
));
if ($extra['request']->user->administrator) { if ($extra['request']->user->administrator) {
$this->fields['staff'] = new Pluf_Form_Field_Boolean( $this->fields['staff'] = new Pluf_Form_Field_Boolean(
array('required' => false, array('required' => false,
@ -136,8 +198,37 @@ class IDF_Form_Admin_UserUpdate extends Pluf_Form
$update_pass = true; $update_pass = true;
} }
$this->user->setFromFormData($this->cleaned_data); $this->user->setFromFormData($this->cleaned_data);
if ($commit) { if ($commit) {
$this->user->update(); $this->user->update();
// FIXME: go the extra mile and check the input lengths for
// all fields here!
// FIXME: this is all doubled in UserAccount!
$user_data = IDF_UserData::factory($this->user);
// Add or remove avatar - we need to do this here because every
// single setter directly leads to a save in the database
if ($user_data->avatar != '' &&
($this->cleaned_data['remove_custom_avatar'] == 1 ||
$this->cleaned_data['custom_avatar'] != '')) {
$avatar_path = Pluf::f('upload_path').'/avatars/'.basename($user_data->avatar);
if (basename($avatar_path) != '' && is_file($avatar_path)) {
unlink($avatar_path);
}
$user_data->avatar = '';
}
if ($this->cleaned_data['custom_avatar'] != '') {
$user_data->avatar = $this->cleaned_data['custom_avatar'];
}
$user_data->description = $this->cleaned_data['description'];
$user_data->twitter = $this->cleaned_data['twitter'];
$user_data->public_email = $this->cleaned_data['public_email'];
$user_data->website = $this->cleaned_data['website'];
if ($update_pass) { if ($update_pass) {
/** /**
* [signal] * [signal]
@ -201,8 +292,19 @@ class IDF_Form_Admin_UserUpdate extends Pluf_Form
return $email; return $email;
} }
function clean_custom_avatar()
{
// Just png, jpeg/jpg or gif
if (!preg_match('/\.(png|jpg|jpeg|gif)$/i', $this->cleaned_data['custom_avatar']) &&
$this->cleaned_data['custom_avatar'] != '') {
@unlink(Pluf::f('upload_path').'/avatars/'.$this->cleaned_data['custom_avatar']);
throw new Pluf_Form_Invalid(__('For security reason, you cannot upload a file with this extension.'));
}
return $this->cleaned_data['custom_avatar'];
}
/** /**
* Check to see if the 2 passwords are the same. * Check to see if the two passwords are the same.
*/ */
public function clean() public function clean()
{ {

View File

@ -33,6 +33,8 @@ class IDF_Form_UserAccount extends Pluf_Form
public function initFields($extra=array()) public function initFields($extra=array())
{ {
$this->user = $extra['user']; $this->user = $extra['user'];
$user_data = IDF_UserData::factory($this->user);
$this->fields['first_name'] = new Pluf_Form_Field_Varchar( $this->fields['first_name'] = new Pluf_Form_Field_Varchar(
array('required' => false, array('required' => false,
'label' => __('First name'), 'label' => __('First name'),
@ -92,6 +94,66 @@ class IDF_Form_UserAccount extends Pluf_Form
), ),
)); ));
$this->fields['description'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Description'),
'initial' => $user_data->description,
'widget_attrs' => array('rows' => 3,
'cols' => 40),
'widget' => 'Pluf_Form_Widget_TextareaInput',
));
$this->fields['twitter'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Twitter username'),
'initial' => $user_data->twitter,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['public_email'] = new Pluf_Form_Field_Email(
array('required' => false,
'label' => __('Public email address'),
'initial' => $user_data->public_email,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['website'] = new Pluf_Form_Field_Url(
array('required' => false,
'label' => __('Website URL'),
'initial' => $user_data->website,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['custom_avatar'] = new Pluf_Form_Field_File(
array('required' => false,
'label' => __('Upload custom avatar'),
'initial' => '',
'max_size' => Pluf::f('max_upload_size', 2097152),
'move_function_params' => array('upload_path' => Pluf::f('upload_path').'/avatars',
'upload_path_create' => true,
'upload_overwrite' => true,
'file_name' => 'user_'.$this->user->id.'_%s'),
'help_text' => __('An image file with a width and height not larger than 60 pixels (bigger images are scaled down).'),
));
$this->fields['remove_custom_avatar'] = new Pluf_Form_Field_Boolean(
array('required' => false,
'label' => __('Remove custom avatar'),
'initial' => false,
'widget' => 'Pluf_Form_Widget_CheckboxInput',
'widget_attrs' => array(),
'help_text' => __('Tick this to delete the custom avatar.'),
));
$this->fields['public_key'] = new Pluf_Form_Field_Varchar( $this->fields['public_key'] = new Pluf_Form_Field_Varchar(
array('required' => false, array('required' => false,
'label' => __('Add a public key'), 'label' => __('Add a public key'),
@ -138,7 +200,7 @@ class IDF_Form_UserAccount extends Pluf_Form
'email' => $new_email, 'email' => $new_email,
'user'=> $this->user, 'user'=> $this->user,
) )
); );
$tmpl = new Pluf_Template('idf/user/changeemail-email.txt'); $tmpl = new Pluf_Template('idf/user/changeemail-email.txt');
$text_email = $tmpl->render($context); $text_email = $tmpl->render($context);
$email = new Pluf_Mail(Pluf::f('from_email'), $new_email, $email = new Pluf_Mail(Pluf::f('from_email'), $new_email,
@ -157,8 +219,37 @@ class IDF_Form_UserAccount extends Pluf_Form
$key->create(); $key->create();
} }
} }
if ($commit) { if ($commit) {
$this->user->update(); $this->user->update();
// FIXME: go the extra mile and check the input lengths for
// all fields here!
// FIXME: this is all doubled in admin/UserUpdate!
$user_data = IDF_UserData::factory($this->user);
// Add or remove avatar - we need to do this here because every
// single setter directly leads to a save in the database
if ($user_data->avatar != '' &&
($this->cleaned_data['remove_custom_avatar'] == 1 ||
$this->cleaned_data['custom_avatar'] != '')) {
$avatar_path = Pluf::f('upload_path').'/avatars/'.basename($user_data->avatar);
if (basename($avatar_path) != '' && is_file($avatar_path)) {
unlink($avatar_path);
}
$user_data->avatar = '';
}
if ($this->cleaned_data['custom_avatar'] != '') {
$user_data->avatar = $this->cleaned_data['custom_avatar'];
}
$user_data->description = $this->cleaned_data['description'];
$user_data->twitter = $this->cleaned_data['twitter'];
$user_data->public_email = $this->cleaned_data['public_email'];
$user_data->website = $this->cleaned_data['website'];
if ($update_pass) { if ($update_pass) {
/** /**
* [signal] * [signal]
@ -266,6 +357,19 @@ class IDF_Form_UserAccount extends Pluf_Form
return $key; return $key;
} }
function clean_custom_avatar()
{
// Just png, jpeg/jpg or gif
if (!preg_match('/\.(png|jpg|jpeg|gif)$/i', $this->cleaned_data['custom_avatar']) &&
$this->cleaned_data['custom_avatar'] != '') {
@unlink(Pluf::f('upload_path').'/avatars/'.$this->cleaned_data['custom_avatar']);
throw new Pluf_Form_Invalid(__('For security reason, you cannot upload a file with this extension.'));
}
return $this->cleaned_data['custom_avatar'];
}
function clean_last_name() function clean_last_name()
{ {
$last_name = trim($this->cleaned_data['last_name']); $last_name = trim($this->cleaned_data['last_name']);
@ -322,4 +426,6 @@ class IDF_Form_UserAccount extends Pluf_Form
return $this->cleaned_data; return $this->cleaned_data;
} }
} }

View File

@ -196,4 +196,9 @@ class IDF_IssueComment extends Pluf_Model
$tmpl = new Pluf_Template('idf/issues/feedfragment.xml'); $tmpl = new Pluf_Template('idf/issues/feedfragment.xml');
return $tmpl->render($context); return $tmpl->render($context);
} }
public function get_submitter_data()
{
return IDF_UserData::factory($this->get_submitter());
}
} }

View File

@ -221,4 +221,9 @@ class IDF_Review_Comment extends Pluf_Model
} }
Pluf_Translation::loadSetLocale($current_locale); Pluf_Translation::loadSetLocale($current_locale);
} }
public function get_submitter_data()
{
return IDF_UserData::factory($this->get_submitter());
}
} }

59
src/IDF/UserData.php Normal file
View File

@ -0,0 +1,59 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2008 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Thin wrapper around the general purpose Gconf data driver
* to model a userdata object as key value store
*/
class IDF_UserData extends IDF_Gconf
{
/** columns for the underlying model for which we do not want to
override __get and __set */
private static $protectedVars =
array('id', 'model_class', 'model_id', 'vkey', 'vdesc');
function __set($key, $value)
{
if (in_array($key, self::$protectedVars))
{
parent::__set($key, $value);
return;
}
$this->setVal($key, $value);
}
function __get($key)
{
if (in_array($key, self::$protectedVars))
return parent::__get($key);
return $this->getVal($key, null);
}
public static function factory($user)
{
$conf = new IDF_UserData();
$conf->setModel((object) array('_model'=>'IDF_UserData', 'id' => $user->id));
$conf->initCache();
return $conf;
}
}

View File

@ -86,7 +86,10 @@ class IDF_Views
$title = __('Create Your Account'); $title = __('Create Your Account');
$params = array('request'=>$request); $params = array('request'=>$request);
if ($request->method == 'POST') { if ($request->method == 'POST') {
$form = new IDF_Form_Register($request->POST, $params); $form = new IDF_Form_Register(array_merge(
(array)$request->POST,
(array)$request->FILES
), $params);
if ($form->isValid()) { if ($form->isValid()) {
$user = $form->save(); // It is sending the confirmation email $user = $form->save(); // It is sending the confirmation email
$url = Pluf_HTTP_URL_urlForView('IDF_Views::registerInputKey'); $url = Pluf_HTTP_URL_urlForView('IDF_Views::registerInputKey');

View File

@ -268,7 +268,9 @@ class IDF_Views_Admin
} }
if ($request->method == 'POST') { if ($request->method == 'POST') {
$form = new IDF_Form_Admin_UserUpdate($request->POST, $params); $form = new IDF_Form_Admin_UserUpdate(array_merge($request->POST,
$request->FILES),
$params);
if ($form->isValid()) { if ($form->isValid()) {
$form->save(); $form->save();
$request->user->setMessage(__('The user has been updated.')); $request->user->setMessage(__('The user has been updated.'));
@ -299,7 +301,9 @@ class IDF_Views_Admin
'request' => $request, 'request' => $request,
); );
if ($request->method == 'POST') { if ($request->method == 'POST') {
$form = new IDF_Form_Admin_UserCreate($request->POST, $params); $form = new IDF_Form_Admin_UserCreate(array_merge($request->POST,
$request->FILES),
$params);
if ($form->isValid()) { if ($form->isValid()) {
$cuser = $form->save(); $cuser = $form->save();
$request->user->setMessage(sprintf(__('The user %s has been created.'), (string) $cuser)); $request->user->setMessage(sprintf(__('The user %s has been created.'), (string) $cuser));

View File

@ -279,6 +279,7 @@ class IDF_Views_Issue
$form = new IDF_Form_IssueUpdate(null, $params); $form = new IDF_Form_IssueUpdate(null, $params);
} }
} }
$arrays = self::autoCompleteArrays($prj); $arrays = self::autoCompleteArrays($prj);
return Pluf_Shortcuts_RenderToResponse('idf/issues/view.html', return Pluf_Shortcuts_RenderToResponse('idf/issues/view.html',
array_merge( array_merge(
@ -290,7 +291,7 @@ class IDF_Views_Issue
'page_title' => $title, 'page_title' => $title,
'closed' => $closed, 'closed' => $closed,
'preview' => $preview, 'preview' => $preview,
'interested' =>$interested->count(), 'interested' => $interested->count(),
), ),
$arrays), $arrays),
$request); $request);

View File

@ -192,6 +192,7 @@ class IDF_Views_Review
$files[$filename] = array('', $form->f->{md5($filename)}, $cts); $files[$filename] = array('', $form->f->{md5($filename)}, $cts);
} }
} }
$reviewers = Pluf_Model_RemoveDuplicates($reviewers); $reviewers = Pluf_Model_RemoveDuplicates($reviewers);
return Pluf_Shortcuts_RenderToResponse('idf/review/view.html', return Pluf_Shortcuts_RenderToResponse('idf/review/view.html',
array_merge( array_merge(

View File

@ -110,7 +110,10 @@ class IDF_Views_User
$ext_pass = substr(sha1($request->user->password.Pluf::f('secret_key')), 0, 8); $ext_pass = substr(sha1($request->user->password.Pluf::f('secret_key')), 0, 8);
$params = array('user' => $request->user); $params = array('user' => $request->user);
if ($request->method == 'POST') { if ($request->method == 'POST') {
$form = new IDF_Form_UserAccount($request->POST, $params); $form = new IDF_Form_UserAccount(array_merge(
(array)$request->POST,
(array)$request->FILES
), $params);
if ($form->isValid()) { if ($form->isValid()) {
$user = $form->save(); $user = $form->save();
$url = Pluf_HTTP_URL_urlForView('IDF_Views_User::myAccount'); $url = Pluf_HTTP_URL_urlForView('IDF_Views_User::myAccount');
@ -121,9 +124,10 @@ class IDF_Views_User
} else { } else {
$data = $request->user->getData(); $data = $request->user->getData();
unset($data['password']); unset($data['password']);
$form = new IDF_Form_UserAccount($data, $params); $form = new IDF_Form_UserAccount(null, $params);
} }
$keys = $request->user->get_idf_key_list(); $keys = $request->user->get_idf_key_list();
return Pluf_Shortcuts_RenderToResponse('idf/user/myaccount.html', return Pluf_Shortcuts_RenderToResponse('idf/user/myaccount.html',
array('page_title' => __('Your Account'), array('page_title' => __('Your Account'),
'api_key' => $api_key, 'api_key' => $api_key,
@ -212,9 +216,13 @@ class IDF_Views_User
if (count($users) != 1 or !$users[0]->active) { if (count($users) != 1 or !$users[0]->active) {
throw new Pluf_HTTP_Error404(); throw new Pluf_HTTP_Error404();
} }
$user = $users[0];
$user_data = IDF_UserData::factory($user);
return Pluf_Shortcuts_RenderToResponse('idf/user/public.html', return Pluf_Shortcuts_RenderToResponse('idf/user/public.html',
array('page_title' => (string) $users[0], array('page_title' => (string) $user,
'member' => $users[0], 'member' => $user,
'user_data' => $user_data,
), ),
$request); $request);
} }

View File

@ -43,6 +43,8 @@ $m['IDF_Conf'] = array('relate_to' => array('IDF_Project'));
$m['IDF_Commit'] = array('relate_to' => array('IDF_Project', 'Pluf_User')); $m['IDF_Commit'] = array('relate_to' => array('IDF_Project', 'Pluf_User'));
$m['IDF_Scm_Cache_Git'] = array('relate_to' => array('IDF_Project')); $m['IDF_Scm_Cache_Git'] = array('relate_to' => array('IDF_Project'));
$m['IDF_UserData'] = array('relate_to' => array('Pluf_User'));
Pluf_Signal::connect('Pluf_Template_Compiler::construct_template_tags_modifiers', Pluf_Signal::connect('Pluf_Template_Compiler::construct_template_tags_modifiers',
array('IDF_Middleware', 'updateTemplateTagsModifiers')); array('IDF_Middleware', 'updateTemplateTagsModifiers'));

View File

@ -9,7 +9,7 @@
{/if} {/if}
</div> </div>
{/if} {/if}
<form method="post" action="."> <form method="post" enctype="multipart/form-data" action=".">
<table class="form" summary=""> <table class="form" summary="">
<tr> <tr>
<th>{trans 'Login:'}</th>{aurl 'url', 'IDF_Views_User::view', array($cuser.login)} <th>{trans 'Login:'}</th>{aurl 'url', 'IDF_Views_User::view', array($cuser.login)}
@ -51,7 +51,48 @@
<td>{if $form.f.password2.errors}{$form.f.password2.fieldErrors}{/if} <td>{if $form.f.password2.errors}{$form.f.password2.fieldErrors}{/if}
{$form.f.password2|unsafe} {$form.f.password2|unsafe}
</td> </td>
</tr>{if $user.administrator} </tr>
<tr><td colspan="2" class="separator">{trans "Public Profile"}</td></tr>
<tr>
<th>{$form.f.description.labelTag}:</th>
<td>{if $form.f.description.errors}{$form.f.description.fieldErrors}{/if}
{$form.f.description|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.twitter.labelTag}:</th>
<td>{if $form.f.twitter.errors}{$form.f.twitter.fieldErrors}{/if}
{$form.f.twitter|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.public_email.labelTag}:</th>
<td>{if $form.f.public_email.errors}{$form.f.public_email.fieldErrors}{/if}
{$form.f.public_email|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.website.labelTag}:</th>
<td>{if $form.f.website.errors}{$form.f.website.fieldErrors}{/if}
{$form.f.website|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.custom_avatar.labelTag}:</th>
<td>{if $form.f.custom_avatar.errors}{$form.f.custom_avatar.fieldErrors}{/if}
{$form.f.custom_avatar|unsafe}<br />
<span class="helptext">{$form.f.custom_avatar.help_text}</span>
</td>
</tr>
<tr>
<th>{if $form.f.remove_custom_avatar.errors}{$form.f.remove_custom_avatar.fieldErrors}{/if}
{$form.f.remove_custom_avatar|unsafe}
</th>
<td>{$form.f.remove_custom_avatar.labelTag}<br />
<span class="helptext">{$form.f.remove_custom_avatar.help_text}</span></td>
</tr>
{if $user.administrator}
<tr><td colspan="2" class="separator">{trans "Administrative"}</td></tr>
<tr> <tr>
<th>{if $form.f.staff.errors}{$form.f.staff.fieldErrors}{/if} <th>{if $form.f.staff.errors}{$form.f.staff.fieldErrors}{/if}
{$form.f.staff|unsafe} {$form.f.staff|unsafe}

View File

@ -3,8 +3,15 @@
{block body} {block body}
{assign $i = 0} {assign $i = 0}
{assign $nc = $comments.count()} {assign $nc = $comments.count()}
{foreach $comments as $c}{ashowuser 'submitter', $c.get_submitter(), $request}{assign $who = $c.get_submitter()} {foreach $comments as $c}{ashowuser 'submitter', $c.get_submitter(), $request}
<div class="issue-comment{if $i == 0} issue-comment-first{/if}{if $i == ($nc-1)} issue-comment-last{/if}" id="ic{$c.id}"><img style="float:right; position: relative;" src="http://www.gravatar.com/avatar/{$who.email|md5}.jpg?s=60&amp;d={media}/idf/img/spacer.gif" alt=" " /> {assign $submitter = $c.get_submitter()}
{assign $submitter_data = $c.get_submitter_data()}
<div class="issue-comment{if $i == 0} issue-comment-first{/if}{if $i == ($nc-1)} issue-comment-last{/if}" id="ic{$c.id}">
{if $submitter_data.avatar != ''}
<img style="float:right; position: relative; max-height: 60px; max-width: 60px;" src="{media}/upload/avatars/{$submitter_data.avatar}" alt=" " />
{else}
<img style="float:right; position: relative; max-height: 60px; max-width: 60px;" src="http://www.gravatar.com/avatar/{$submitter.email|md5}.jpg?s=60&amp;d={media}/idf/img/spacer.gif" alt=" " />
{/if}
{if $i == 0} {if $i == 0}
<p>{blocktrans}Reported by {$submitter}, {$c.creation_dtime|date}{/blocktrans}</p> <p>{blocktrans}Reported by {$submitter}, {$c.creation_dtime|date}{/blocktrans}</p>
{else} {else}

View File

@ -106,8 +106,13 @@ to propose more contributions</strong>.
{if $nc}<hr align="left" class="attach" /> {if $nc}<hr align="left" class="attach" />
<h2>{trans 'General Comments'}</h2> <h2>{trans 'General Comments'}</h2>
{/if} {/if}
{foreach $comments as $c}{ashowuser 'submitter', $c.get_submitter(), $request}{assign $who = $c.get_submitter()} {foreach $comments as $c}{ashowuser 'submitter', $c.get_submitter(), $request}{assign $submitter = $c.get_submitter()}{assign $submitter_data = $c.get_submitter_data()}
<div class="issue-comment{if $i == 1} issue-comment-first{/if}{if $i == ($nc)} issue-comment-last{/if}" id="ic{$c.id}"><img style="float:right; position: relative;" src="http://www.gravatar.com/avatar/{$who.email|md5}.jpg?s=60&amp;d={media}/idf/img/spacer.gif" alt=" " /> <div class="issue-comment{if $i == 1} issue-comment-first{/if}{if $i == ($nc)} issue-comment-last{/if}" id="ic{$c.id}">
{if $submitter_data.avatar != ''}
<img style="float:right; position: relative; max-height: 60px; max-width: 60px;" src="{media}/upload/avatars/{$submitter_data.avatar}" alt=" " />
{else}
<img style="float:right; position: relative;" src="http://www.gravatar.com/avatar/{$submitter.email|md5}.jpg?s=60&amp;d={media}/idf/img/spacer.gif" alt=" " />
{/if}
{aurl 'url', 'IDF_Views_Review::view', array($project.shortname, $review.id)} {aurl 'url', 'IDF_Views_Review::view', array($project.shortname, $review.id)}
{assign $id = $c.id} {assign $id = $c.id}
{assign $url = $url~'#ic'~$c.id} {assign $url = $url~'#ic'~$c.id}

View File

@ -9,7 +9,7 @@
</div> </div>
{/if} {/if}
<form method="post" action="."> <form method="post" enctype="multipart/form-data" action=".">
<table class="form" summary=""> <table class="form" summary="">
<tr> <tr>
<th>{trans 'Login:'}</th>{aurl 'url', 'IDF_Views_User::view', array($user.login)} <th>{trans 'Login:'}</th>{aurl 'url', 'IDF_Views_User::view', array($user.login)}
@ -53,6 +53,46 @@
{$form.f.password2|unsafe} {$form.f.password2|unsafe}
</td> </td>
</tr> </tr>
<tr><td colspan="2" class="separator">{trans "Public Profile"}</td></tr>
<tr>
<th>{$form.f.description.labelTag}:</th>
<td>{if $form.f.description.errors}{$form.f.description.fieldErrors}{/if}
{$form.f.description|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.twitter.labelTag}:</th>
<td>{if $form.f.twitter.errors}{$form.f.twitter.fieldErrors}{/if}
{$form.f.twitter|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.public_email.labelTag}:</th>
<td>{if $form.f.public_email.errors}{$form.f.public_email.fieldErrors}{/if}
{$form.f.public_email|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.website.labelTag}:</th>
<td>{if $form.f.website.errors}{$form.f.website.fieldErrors}{/if}
{$form.f.website|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.custom_avatar.labelTag}:</th>
<td>{if $form.f.custom_avatar.errors}{$form.f.custom_avatar.fieldErrors}{/if}
{$form.f.custom_avatar|unsafe}<br />
<span class="helptext">{$form.f.custom_avatar.help_text}</span>
</td>
</tr>
<tr>
<th>{if $form.f.remove_custom_avatar.errors}{$form.f.remove_custom_avatar.fieldErrors}{/if}
{$form.f.remove_custom_avatar|unsafe}
</th>
<td>{$form.f.remove_custom_avatar.labelTag}<br />
<span class="helptext">{$form.f.remove_custom_avatar.help_text}</span></td>
</tr>
<tr><td colspan="2" class="separator">{trans "Key Management"}</td></tr>
<tr> <tr>
<th>{$form.f.public_key.labelTag}:</th> <th>{$form.f.public_key.labelTag}:</th>
<td>{if $form.f.public_key.errors}{$form.f.public_key.fieldErrors}{/if} <td>{if $form.f.public_key.errors}{$form.f.public_key.fieldErrors}{/if}

View File

@ -2,9 +2,39 @@
{block body} {block body}
<table class="form" summary=""> <table class="form" summary="">
<tr> <tr>
<th></th> <th style="text-align: right">{if $user_data.avatar != ''}
<img style="max-height: 60px; max-width: 60px;" src="{media}/upload/avatars/{$user_data.avatar}" alt=" " />
{else}
<img src="http://www.gravatar.com/avatar/{$member.email|md5}.jpg?s=60&amp;d={media}/idf/img/spacer.gif" alt=" " />
{/if}
</th>
<td>{$member}</td> <td>{$member}</td>
</tr> </tr>
{if $user_data.description != ''}
<tr>
<th>{trans 'Description:'}</th>
<td>{$user_data.description}</td>
</tr>
{/if}
{if $user_data.twitter != ''}
<tr>
<th>{trans 'Twitter:'}</th>
<td><a rel="nofollow" target="_blank" href='http://twitter.com/{$user_data.twitter}'>{$user_data.twitter}</a></td>
</tr>
{/if}
{if $user_data.public_email != ''}
<tr>
<th>{trans 'Public Email:'}</th>
<td><a href='mailto:{$user_data.public_email}'>{$user_data.public_email}</a></td>
</tr>
{/if}
{if $user_data.website != ''}
<tr>
<th>{trans 'Website:'}</th>
<td><a rel="nofollow" target="_blank" href='{$user_data.website}'>{$user_data.website}</a></td>
</tr>
{/if}
<tr> <tr>
<th>{trans 'Last time seen:'}</th> <th>{trans 'Last time seen:'}</th>
<td>{$member.last_login|dateago}</td> <td>{$member.last_login|dateago}</td>

View File

@ -104,6 +104,11 @@ table.form th {
font-weight: normal; font-weight: normal;
} }
table.form td.separator {
font-weight: bold;
text-align: center;
}
.px-message-error { .px-message-error {
padding-left: 37px; padding-left: 37px;
background: url("../img/dialog-error.png"); background: url("../img/dialog-error.png");