Merge pull request #1556 from opf/feature/api-create-user-show

Feature/api create user show
pull/1562/head
Alex Coles 10 years ago
commit e9bd084ae2
  1. 48
      app/assets/javascripts/angular/work_packages/controllers/work-package-details-controller.js
  2. 43
      app/assets/javascripts/angular/work_packages/tabs/user_field-directive.js
  3. 6
      config/locales/js-de.yml
  4. 6
      config/locales/js-en.yml
  5. 7
      lib/api/v3/root.rb
  6. 1
      lib/api/v3/users/user_model.rb
  7. 5
      lib/api/v3/users/user_representer.rb
  8. 30
      lib/api/v3/users/users_api.rb
  9. 12
      lib/api/v3/work_packages/work_package_model.rb
  10. 39
      lib/api/v3/work_packages/work_package_representer.rb
  11. 109
      public/templates/tabs/overview.html
  12. 11
      public/templates/work_packages/tabs/_user_field.html
  13. 23
      public/templates/work_packages/tabs/overview.html
  14. 58
      spec/api/representers/user_representer_spec.rb
  15. 14
      spec/representers/work_package_representer_spec.rb

@ -29,20 +29,22 @@
angular.module('openproject.workPackages.controllers')
.constant('DEFAULT_WORK_PACKAGE_PROPERTIES', [
'status', 'assigneeName', 'responsibleName',
'status', 'assignee', 'responsible',
'date', 'percentageDone', 'priority',
'authorName', 'createdAt', 'dueDate',
'author', 'createdAt', 'dueDate',
'estimatedTime', 'startDate', 'updatedAt',
'versionName'
])
.constant('USER_TYPE', 'user')
.controller('WorkPackageDetailsController', [
'$scope',
'workPackage',
'I18n',
'DEFAULT_WORK_PACKAGE_PROPERTIES',
'USER_TYPE',
'WorkPackagesHelper',
function($scope, workPackage, I18n, DEFAULT_WORK_PACKAGE_PROPERTIES, WorkPackagesHelper) {
function($scope, workPackage, I18n, DEFAULT_WORK_PACKAGE_PROPERTIES, USER_TYPE, WorkPackagesHelper) {
// initialization
$scope.workPackage = workPackage;
$scope.$parent.preselectedWorkPackageId = $scope.workPackage.props.id;
@ -59,6 +61,14 @@ angular.module('openproject.workPackages.controllers')
var workPackageProperties = DEFAULT_WORK_PACKAGE_PROPERTIES;
function getPropertyValue(property, format) {
if (format === USER_TYPE) {
return workPackage.embedded[property];
} else {
return getFormattedPropertyValue(property);
}
}
function getFormattedPropertyValue(property) {
if (property === 'date') {
if (workPackage.props.startDate && workPackage.props.dueDate) {
@ -72,11 +82,12 @@ angular.module('openproject.workPackages.controllers')
}
}
function addFormattedValueToPresentProperties(property, label, value) {
function addFormattedValueToPresentProperties(property, label, value, format) {
$scope.presentWorkPackageProperties.push({
property: property,
label: label,
value: value || '-'
value: value || '-',
format: format
});
}
@ -91,18 +102,23 @@ angular.module('openproject.workPackages.controllers')
});
}
angular.forEach(workPackageProperties, function(property, index) {
var label = I18n.t('js.work_packages.properties.' + property),
value = getFormattedPropertyValue(property);
var userFields = ['assignee', 'author', 'responsible'];
if (!!value ||
index < 3 ||
index < 6 && secondRowToBeDisplayed()) {
addFormattedValueToPresentProperties(property, label, value);
} else {
$scope.emptyWorkPackageProperties.push(label);
}
});
(function setupWorkPackageProperties() {
angular.forEach(workPackageProperties, function(property, index) {
var label = I18n.t('js.work_packages.properties.' + property),
format = userFields.indexOf(property) === -1 ? 'text' : USER_TYPE,
value = getPropertyValue(property, format);
if (!!value ||
index < 3 ||
index < 6 && secondRowToBeDisplayed()) {
addFormattedValueToPresentProperties(property, label, value, format);
} else {
$scope.emptyWorkPackageProperties.push(label);
}
});
})();
// toggles
$scope.toggleStates = {

@ -0,0 +1,43 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program 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.
//
// This program 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 Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
angular.module('openproject.workPackages.tabs')
.directive('userField', [function() {
return {
restrict: 'E',
replace: true,
templateUrl: '/templates/work_packages/tabs/_user_field.html',
scope: { user: '=' },
link: function(scope) {
if (scope.user.props && (scope.user.props.firstName || scope.user.props.lastName)) {
scope.userName = scope.user.props.firstName + ' ' + scope.user.props.lastName;
}
}
};
}]);

@ -217,8 +217,8 @@ de:
message_error_during_bulk_delete: Fehler beim Löschen der Arbeitspakete.
message_successful_bulk_delete: Arbeitspakete erfolgreich gelöscht.
properties:
assigneeName: "Zugewiesen an"
authorName: "Autor"
assignee: "Zugewiesen an"
author: "Autor"
createdAt: "Angelegt"
description: "Beschreibung"
date: "Datum"
@ -227,7 +227,7 @@ de:
percentageDone: "% erledigt"
priority: "Priorität"
projectName: "Projekt"
responsibleName: "Verantwortlicher"
responsible: "Verantwortlicher"
startDate: "Startdatum"
status: "Status"
subject: "Thema"

@ -220,8 +220,8 @@ en:
message_error_during_bulk_delete: An error occurred while trying to delete work packages.
message_successful_bulk_delete: Successfully deleted work packages.
properties:
assigneeName: "Assignee"
authorName: "Author"
assignee: "Assignee"
author: "Author"
createdAt: "Created on"
description: "Description"
date: "Datum"
@ -230,7 +230,7 @@ en:
percentageDone: "Percentage done"
priority: "Priority"
projectName: "Project"
responsibleName: "Responsible"
responsible: "Responsible"
startDate: "Start date"
status: "Status"
subject: "Subject"

@ -35,11 +35,12 @@ module API
module V3
class Root < Grape::API
version 'v3', using: :path
mount ::API::V3::Activities::ActivitiesAPI
mount ::API::V3::Attachments::AttachmentsAPI
mount ::API::V3::WorkPackages::WorkPackagesAPI
mount ::API::V3::Queries::QueriesAPI
mount ::API::V3::Activities::ActivitiesAPI
mount ::API::V3::Users::UsersAPI
mount ::API::V3::WorkPackages::WorkPackagesAPI
end
end
end

@ -35,6 +35,7 @@ module API
module Users
class UserModel < Reform::Form
include Coercion
include GravatarImageTag
property :login, type: String
property :firstname, type: String

@ -50,9 +50,10 @@ module API
property :id, getter: -> (*) { model.id }, render_nil: true
property :login, render_nil: true
property :firstname, render_nil: true
property :lastname, render_nil: true
property :firstname, as: :firstName, render_nil: true
property :lastname, as: :lastName, render_nil: true
property :mail, render_nil: true
property :avatar, getter: ->(*) { gravatar_image_url(mail) }, render_nil: true
property :created_at, getter: -> (*) { model.created_on.utc.iso8601 }, render_nil: true
property :updated_at, getter: -> (*) { model.updated_on.utc.iso8601 }, render_nil: true

@ -0,0 +1,30 @@
module API
module V3
module Users
class UsersAPI < Grape::API
resources :users do
params do
requires :id, desc: 'User\'s id'
end
namespace ':id' do
before do
@user = User.find(params[:id])
model = ::API::V3::Users::UserModel.new(@user)
@representer = ::API::V3::Users::UserRepresenter.new(model)
end
get do
@representer.to_json
end
end
end
end
end
end
end

@ -104,6 +104,18 @@ module API
work_package.done_ratio = value
end
def author
::API::V3::Users::UserModel.new(work_package.author) unless work_package.author.nil?
end
def responsible
::API::V3::Users::UserModel.new(work_package.responsible) unless work_package.responsible.nil?
end
def assignee
::API::V3::Users::UserModel.new(work_package.assigned_to) unless work_package.assigned_to.nil?
end
def activities
work_package.journals.map{ |journal| ::API::V3::Activities::ActivityModel.new(journal: journal) }
end

@ -51,6 +51,27 @@ module API
{ href: "#{root_url}api/v3/work_packages/#{represented.work_package.id}", title: "#{represented.subject}" }
end
link :author do
{
href: "#{root_url}/api/v3/users/#{represented.work_package.author.id}",
title: "#{represented.work_package.author.name} - #{represented.work_package.author.login}"
} unless represented.work_package.author.nil?
end
link :responsible do
{
href: "#{root_url}/api/v3/users/#{represented.work_package.responsible.id}",
title: "#{represented.work_package.responsible.name} - #{represented.work_package.responsible.login}"
} unless represented.work_package.responsible.nil?
end
link :assignee do
{
href: "#{root_url}/api/v3/users/#{represented.work_package.assigned_to.id}",
title: "#{represented.work_package.assigned_to.name} - #{represented.work_package.assigned_to.login}"
} unless represented.work_package.assigned_to.nil?
end
property :id, getter: -> (*) { work_package.id }, render_nil: true
property :subject, render_nil: true
property :type, render_nil: true
@ -65,25 +86,15 @@ module API
property :version_name, getter: -> (*) { work_package.fixed_version.try(:name) }, render_nil: true
property :project_id, getter: -> (*) { work_package.project.id }
property :project_name, getter: -> (*) { work_package.project.try(:name) }
property :responsible_id, getter: -> (*) { work_package.responsible.try(:id) }, render_nil: true
property :responsible_name, getter: -> (*) { work_package.responsible.try(:name) }, render_nil: true
property :responsible_login, getter: -> (*) { work_package.responsible.try(:login) }, render_nil: true
property :responsible_mail, getter: -> (*) { work_package.responsible.try(:mail) }, render_nil: true
property :responsible_avatar, getter: -> (*) { gravatar_image_url(work_package.responsible.try(:mail)) }, render_nil: true
property :assigned_to_id, as: :assigneeId, getter: -> (*) { work_package.assigned_to.try(:id) }, render_nil: true
property :assignee_name, getter: -> (*) { work_package.assigned_to.try(:name) }, render_nil: true
property :assignee_login, getter: -> (*) { work_package.assigned_to.try(:login) }, render_nil: true
property :assignee_mail, getter: -> (*) { work_package.assigned_to.try(:mail) }, render_nil: true
property :assignee_avatar, getter: -> (*) { gravatar_image_url(work_package.assigned_to.try(:mail)) }, render_nil: true
property :author_name, getter: -> (*) { work_package.author.name }, render_nil: true
property :author_login, getter: -> (*) { work_package.author.login }, render_nil: true
property :author_mail, getter: -> (*) { work_package.author.mail }, render_nil: true
property :author_avatar, getter: -> (*) { gravatar_image_url(work_package.author.try(:mail)) }, render_nil: true
property :created_at, getter: -> (*) { work_package.created_at.utc.iso8601}, render_nil: true
property :updated_at, getter: -> (*) { work_package.updated_at.utc.iso8601}, render_nil: true
collection :custom_properties, exec_context: :decorator, render_nil: true
property :author, embedded: true, class: ::API::V3::Users::UserModel, decorator: ::API::V3::Users::UserRepresenter, if: -> (*) { !author.nil? }
property :responsible, embedded: true, class: ::API::V3::Users::UserModel, decorator: ::API::V3::Users::UserRepresenter, if: -> (*) { !responsible.nil? }
property :assignee, embedded: true, class: ::API::V3::Users::UserModel, decorator: ::API::V3::Users::UserRepresenter, if: -> (*) { !assignee.nil? }
collection :activities, embedded: true, class: ::API::V3::Activities::ActivityModel, decorator: ::API::V3::Activities::ActivityRepresenter
collection :watchers, embedded: true, class: ::API::V3::Users::UserModel, decorator: ::API::V3::Users::UserRepresenter

@ -0,0 +1,109 @@
<div class="detail-panel-description">
<h3>Description</h3>
<div class="detail-panel-description-content">
{{ workPackage.props.description }}
</div>
</div>
<div class="panel-toggler" ng-click="toggleStates.hideFullDescription = !toggleStates.hideFullDescription">
<fieldset>
<legend align="center">
<span ng-if="!toggleStates.hideFullDescription">
<i class="icon-arrow-right5-2"></i>
Hide full description
</span>
<span ng-if="toggleStates.hideFullDescription">
<i class="icon-arrow-right5-3"></i>
Show full description
</span>
</legend>
</fieldset>
</div>
<div class="detail-panel-attributes" slide-toggle collapsed="toggleStates.hideFullDescription">
<ul>
<li><label>Status</label>{{ workPackage.props.status }}</li>
<li><label>Priortiy</label>{{ workPackage.props.priority }}</li>
<li><label>Date</label>{{ workPackage.props.startDate }} - {{ workPackage.props.dueDate }}</li>
<li>
<label>Responsible</label>
<img class="avatar" ng-src="{{ responsible.props.avatar }}" ng-show="{{ !!responsible }}" />
<span class="user" ng-show="{{ !!responsible }}">
<a ng-href="{{ userPath(responsible.props.id) }}">
{{ responsible.props.firstName }} {{ responsible.props.lastName }}
aaa
</a>
</span>
<span class="user" ng-hide="{{ !!responsible }}"> - </span>
<!-- <span class="role">{{ responsible.props.role }}</span> -->
</li>
<li>
<label>Assignee</label>
<img class="avatar" ng-src="{{ assignee.props.avatar }}" ng-show="{{ !!assignee }}" />
<span class="user" ng-show="{{ !!assignee }}">
<a ng-href="{{ userPath(assignee.props.id) }}">
{{ assignee.props.firstName }} {{ assignee.props.lastName }}
</a>
</span>
<span class="user" ng-hide="{{ !!assignee }}"> - </span>
<!-- <span class="role">{{ assignee.props.roles }}</span> -->
</li>
<li><label>% Done</label>{{ workPackage.props.percentageDone }} %</li>
</ul>
</div>
<div class="panel-toggler" ng-click="toggleStates.hideAllAttributes = !toggleStates.hideAllAttributes">
<fieldset>
<legend align="center">
<span ng-if="!toggleStates.hideAllAttributes">
<i class="icon-arrow-right5-2"></i>
Hide all attributes
</span>
<span ng-if="toggleStates.hideAllAttributes">
<i class="icon-arrow-right5-3"></i>
Show all attributes
</span>
</legend>
</fieldset>
</div>
<div class="detail-panel-latest-activity" slide-toggle collapsed="toggleStates.hideAllAttributes">
<h3>Latest activity</h3>
<ul>
<li>
<div class="comments-number"><a href="#1">#1</a>
<div class="comments-icons"><i class="icon-quote"></i><i class="icon-edit"></i></div>
</div>
<img class="avatar" src="images/avatar_logout.png" />
<span class="user"><a href="#">Christoph Zierz</a></span>
<span class="date">commented on 06/05/2014 16:42 Uhr</span>
<span class="comment">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis . ..
</span>
</li>
<li>
<div class="comments-number"><a href="#1">#2</a>
<div class="comments-icons"><i class="icon-quote"></i><i class="icon-edit"></i></div>
</div>
<img class="avatar" src="images/avatar_logout.png" />
<span class="user"><a href="#">Niels Lindenthal</a></span>
<span class="date">commented on 08/05/2014 16:42 Uhr</span>
<span class="comment">
<ul>
<li>Status changed from scheduled to closed</li>
<li>Assignee deleted (Michael Frister)</li>
<li>% done changed from 0 to 100</li>
</ul>
</span>
</li>
</ul>
</div>
<div class="comments-form">
<h3>Add your comments here</h3>
<form>
<textarea placeholder="Add comments here" rows="4"></textarea>
</form>
<button class="button">Add comment</button>
</div>

@ -0,0 +1,11 @@
<p>
<span ng-if="userName">
<img class="avatar"
ng-if="user.props.avatar"
ng-src="{{user.props.avatar}}" />
<span class="user"><a href ng-bind="userName"/></span>
<span class="role" ng-if="user.props.role" ng-bind="user.props.role"/>
</span>
<span ng-if="!userName"> - </span>
</p>

@ -22,27 +22,8 @@
<ul>
<li ng-repeat="propertyData in presentWorkPackageProperties">
<label ng-bind="propertyData.label"/>
<span ng-switch="propertyData.property">
<p ng-switch-when="assigneeName">
<img class="avatar"
ng-if="workPackage.props.assigneeName && workPackage.props.assigneeAvatar"
ng-src="{{ workPackage.props.assigneeAvatar }}" />
<span class="user"><a href ng-bind="propertyData.value"/></span>
<span class="role" ng-bind="workPackage.props.assigneeRole"/>
</p>
<p ng-switch-when="authorName">
<img class="avatar"
ng-if="workPackage.props.authorName && workPackage.props.authorAvatar"
ng-src="{{ workPackage.props.authorAvatar }}" />
<span class="user"><a href ng-bind="propertyData.value"/></span>
</p>
<p ng-switch-when="responsibleName">
<img class="avatar"
ng-if="workPackage.props.responsibleName && workPackage.props.responsibleAvatar"
ng-src="{{ workPackage.props.responsibleAvatar }}" />
<span class="user"><a href ng-bind="propertyData.value"/></span>
<span class="role" ng-bind="workPackage.props.responsibleRole"/>
</p>
<span ng-switch="propertyData.format">
<user-field ng-switch-when="user" user="propertyData.value"></user-field>
<span ng-switch-default ng-bind="propertyData.value"/>
</span>
</li>

@ -0,0 +1,58 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program 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.
#
# This program 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 Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe ::API::V3::Users::UserRepresenter do
let(:user) { FactoryGirl.create(:user) }
let(:model) { ::API::V3::Users::UserModel.new(user) }
let(:representer) { described_class.new(model) }
context 'generation' do
subject(:generated) { representer.to_json }
it { should include_json('User'.to_json).at_path('_type') }
describe 'user' do
it { should have_json_path('id') }
it { should have_json_path('login') }
it { should have_json_path('firstName') }
it { should have_json_path('lastName') }
it { should have_json_path('mail') }
it { should have_json_path('avatar') }
it { should have_json_path('createdAt') }
it { should have_json_path('updatedAt') }
end
describe '_links' do
it 'should link to self' do
expect(subject).to have_json_path('_links/self/href')
end
end
end
end

@ -48,15 +48,6 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
describe 'work_package' do
it { should have_json_path('id') }
it { should have_json_path('assigneeId') }
it { should have_json_path('assigneeLogin') }
it { should have_json_path('assigneeMail') }
it { should have_json_path('assigneeName') }
it { should have_json_path('authorLogin') }
it { should have_json_path('authorMail') }
it { should have_json_path('authorName') }
it { should have_json_path('description') }
it { should have_json_path('dueDate') }
@ -66,11 +57,6 @@ describe ::API::V3::WorkPackages::WorkPackageRepresenter do
it { should have_json_path('projectId') }
it { should have_json_path('projectName') }
it { should have_json_path('responsibleId') }
it { should have_json_path('responsibleLogin') }
it { should have_json_path('responsibleMail') }
it { should have_json_path('responsibleName') }
it { should have_json_path('startDate') }
it { should have_json_path('status') }
it { should have_json_path('subject') }

Loading…
Cancel
Save