Added multi avatar support.

This commit is contained in:
Martin Edenhofer 2014-12-01 08:32:35 +01:00
parent 2c25123824
commit aed7e70179
16 changed files with 790 additions and 226 deletions

View file

@ -1,19 +1,30 @@
class Index extends App.Controller class Index extends App.Controller
elements: elements:
'.js-upload': 'fileInput' '.js-upload': 'fileInput'
'.avatar-gallery': 'avatarGallery' '.avatar-gallery': 'avatarGallery'
events: events:
'click .js-openCamera': 'openCamera' 'click .js-openCamera': 'openCamera'
'change .js-upload': 'onUpload' 'change .js-upload': 'onUpload'
'click .avatar': 'onSelect' 'click .avatar': 'onSelect'
'click .avatar-delete': 'onDelete' 'click .avatar-delete': 'onDelete'
constructor: -> constructor: ->
super super
return if !@authenticate() return if !@authenticate()
@avatars = []
@loadAvatarList()
@render() loadAvatarList: =>
@ajax(
id: 'avatar_list'
type: 'GET'
url: @apiPath + '/users/avatar'
processData: true
success: (data, status, xhr) =>
@avatars = data.avatars
@render()
)
# check if the browser supports webcam access # check if the browser supports webcam access
# doesnt render the camera button if not # doesnt render the camera button if not
@ -24,6 +35,8 @@ class Index extends App.Controller
render: => render: =>
@html App.view('profile/avatar') @html App.view('profile/avatar')
webcamSupport: @hasGetUserMedia() webcamSupport: @hasGetUserMedia()
avatars: @avatars
@$('.avatar[data-id="' + @Session.get('id') + '"]').attr('data-id', '').attr('data-avatar-id', '0')
onSelect: (e) => onSelect: (e) =>
@pick( $(e.currentTarget) ) @pick( $(e.currentTarget) )
@ -31,33 +44,99 @@ class Index extends App.Controller
onDelete: (e) => onDelete: (e) =>
e.stopPropagation() e.stopPropagation()
if confirm App.i18n.translateInline('Delete Avatar?') if confirm App.i18n.translateInline('Delete Avatar?')
params =
id: $(e.currentTarget).parent('.avatar-holder').find('.avatar').data('avatar-id')
$(e.currentTarget).parent('.avatar-holder').remove() $(e.currentTarget).parent('.avatar-holder').remove()
@pick @('.avatar').last() @pick @$('.avatar').last()
# remove avatar
# remove avatar globally
@ajax(
id: 'avatar_delete'
type: 'DELETE'
url: @apiPath + '/users/avatar'
data: JSON.stringify( params )
processData: true
success: (data, status, xhr) =>
)
pick: (avatar) => pick: (avatar) =>
@$('.avatar').removeClass('is-active') @$('.avatar').removeClass('is-active')
avatar.addClass('is-active') avatar.addClass('is-active')
avatar_id = avatar.data('avatar-id')
params =
id: avatar_id
# update avatar globally # update avatar globally
@ajax(
id: 'avatar_set_default'
type: 'POST'
url: @apiPath + '/users/avatar/set'
data: JSON.stringify( params )
processData: true
success: (data, status, xhr) =>
# update avatar in app at runtime
activeAvatar = @$('.avatar.is-active')
style = activeAvatar.attr('style')
# set correct background size
if activeAvatar.text()
style += ';background-size:auto'
else
style += ';background-size:cover'
# find old avatars and update them
replaceAvatar = $('.avatar[data-id="' + @Session.get('id') + '"]')
replaceAvatar.attr('style', style)
# update avatar text if needed
if activeAvatar.text()
replaceAvatar.text(activeAvatar.text())
replaceAvatar.addClass('unique')
else
replaceAvatar.text( '' )
replaceAvatar.removeClass('unique')
)
avatar
openCamera: => openCamera: =>
new Camera new Camera
callback: @storeImage callback: @storeImage
storeImage: (src) => storeImage: (src) =>
avatarHolder = $(App.view('profile/avatar-holder') src: src)
@avatarGallery.append(avatarHolder) # store avatar globally
@pick avatarHolder.find('.avatar') params =
avatar_full: src
# add resized image
avatar = new App.ImageService( src )
params['avatar_resize'] = avatar.toDataURLForAvatar( 'auto', 160 )
# store on server site
@ajax(
id: 'avatar_new'
type: 'POST'
url: @apiPath + '/users/avatar'
data: JSON.stringify( params )
processData: true
success: (data, status, xhr) =>
avatarHolder = $(App.view('profile/avatar-holder')( src: src, avatar: data.avatar ) )
@avatarGallery.append(avatarHolder)
@pick avatarHolder.find('.avatar')
)
onUpload: (event) => onUpload: (event) =>
callback = @storeImage callback = @storeImage
EXIF.getData event.target.files[0], -> EXIF.getData event.target.files[0], ->
orientation = this.exifdata.Orientation orientation = this.exifdata.Orientation
reader = new FileReader() reader = new FileReader()
reader.onload = (e) => reader.onload = (e) =>
new ImageCropper new ImageCropper
imageSource: e.target.result imageSource: e.target.result
callback: callback callback: callback
orientation: orientation orientation: orientation
reader.readAsDataURL(this) reader.readAsDataURL(this)
@ -71,9 +150,9 @@ class ImageCropper extends App.ControllerModal
constructor: (options) -> constructor: (options) ->
super super
@head = 'Crop Image' @head = 'Crop Image'
@cancel = true @cancel = true
@button = 'Save' @button = 'Save'
@buttonClass = 'btn--success' @buttonClass = 'btn--success'
@show( App.view('profile/imageCropper')() ) @show( App.view('profile/imageCropper')() )
@ -97,9 +176,9 @@ class ImageCropper extends App.ControllerModal
@image.attr src: @options.imageSource @image.attr src: @options.imageSource
orientateImage: (e) => orientateImage: (e) =>
image = e.currentTarget image = e.currentTarget
canvas = document.createElement('canvas') canvas = document.createElement('canvas')
ctx = canvas.getContext('2d') ctx = canvas.getContext('2d')
# fit image into options.max bounding box # fit image into options.max bounding box
# if image.width > @options.max # if image.width > @options.max
@ -110,10 +189,10 @@ class ImageCropper extends App.ControllerModal
# image.height = @options.max # image.height = @options.max
if @angle is 180 if @angle is 180
canvas.width = image.width canvas.width = image.width
canvas.height = image.height canvas.height = image.height
else else
canvas.width = image.height canvas.width = image.height
canvas.height = image.width canvas.height = image.width
ctx.translate(canvas.width/2, canvas.height/2) ctx.translate(canvas.width/2, canvas.height/2)
@ -143,25 +222,25 @@ class ImageCropper extends App.ControllerModal
class Camera extends App.ControllerModal class Camera extends App.ControllerModal
elements: elements:
'.js-shoot': 'shootButton' '.js-shoot': 'shootButton'
'.js-submit': 'submitButton' '.js-submit': 'submitButton'
'.camera-preview': 'preview' '.camera-preview': 'preview'
'.camera': 'camera' '.camera': 'camera'
'video': 'video' 'video': 'video'
events: events:
'click .js-shoot:not(.is-disabled)': 'onShootClick' 'click .js-shoot:not(.is-disabled)': 'onShootClick'
constructor: (options) -> constructor: (options) ->
super super
@size = 256 @size = 256
@photoTaken = false @photoTaken = false
@backgroundColor = 'white' @backgroundColor = 'white'
@head = 'Camera' @head = 'Camera'
@cancel = true @cancel = true
@button = 'Save' @button = 'Save'
@buttonClass = 'btn--success is-disabled' @buttonClass = 'btn--success is-disabled'
@centerButtons = [{ @centerButtons = [{
className: 'btn--success js-shoot', className: 'btn--success js-shoot',
text: 'Shoot' text: 'Shoot'
@ -179,19 +258,19 @@ class Camera extends App.ControllerModal
onShootClick: => onShootClick: =>
if @photoTaken if @photoTaken
@photoTaken = false @photoTaken = false
@countdown = 0 @countdown = 0
@submitButton.addClass('is-disabled') @submitButton.addClass('is-disabled')
@shootButton @shootButton
.removeClass('btn--danger') .removeClass('btn--danger')
.addClass('btn--success') .addClass('btn--success')
.text(App.i18n.translateInline('Shoot')) .text( App.i18n.translateInline('Shoot') )
@updatePreview() @updatePreview()
else else
@shoot() @shoot()
@shootButton @shootButton
.removeClass('btn--success') .removeClass('btn--success')
.addClass('btn--danger') .addClass('btn--danger')
.text(App.i18n.translateInline('Discard')) .text( App.i18n.translateInline('Discard') )
shoot: => shoot: =>
@photoTaken = true @photoTaken = true
@ -226,7 +305,7 @@ class Camera extends App.ControllerModal
return return
convertToHumanReadable = convertToHumanReadable =
'PermissionDeniedError': App.i18n.translateInline('You have to allow access to your webcam.') 'PermissionDeniedError': App.i18n.translateInline('You have to allow access to your webcam.')
'ConstraintNotSatisfiedError': App.i18n.translateInline('No camera found.') 'ConstraintNotSatisfiedError': App.i18n.translateInline('No camera found.')
alert convertToHumanReadable[error.name] alert convertToHumanReadable[error.name]
@ -289,13 +368,14 @@ class Camera extends App.ControllerModal
@video.attr height: '' @video.attr height: ''
@cache.attr @cache.attr
width: @video.height() width: @video.height()
height: @video.height() height: @video.height()
offsetX = (@video.width() - @video.height())/2 offsetX = (@video.width() - @video.height())/2
# draw full resolution screenshot # draw full resolution screenshot
@cacheCtx.save() @cacheCtx.save()
# flip image # flip image
@cacheCtx.scale(-1,1) @cacheCtx.scale(-1,1)
@cacheCtx.drawImage(@video.get(0), offsetX, 0, -@video.width(), @video.height()) @cacheCtx.drawImage(@video.get(0), offsetX, 0, -@video.width(), @video.height())

View file

@ -5,7 +5,7 @@ class App.ImageService
src: (url) => src: (url) =>
@orgDataURL = url @orgDataURL = url
resize: ( x = 'auto', y = 'auto') => resize: ( x = 'auto', y = 'auto', sizeFactor = 1) =>
@canvas = document.createElement('canvas') @canvas = document.createElement('canvas')
context = @canvas.getContext('2d') context = @canvas.getContext('2d')
@ -28,7 +28,10 @@ class App.ImageService
factor = imageWidth / y factor = imageWidth / y
x = imageHeight / factor x = imageHeight / factor
console.log('BB', x, y) if x < imageWidth || y < imageHeight
x = x * sizeFactor
y = y * sizeFactor
# set canvas dimensions # set canvas dimensions
@canvas.width = x @canvas.width = x
@canvas.height = y @canvas.height = y
@ -47,10 +50,10 @@ class App.ImageService
toDataURLForAvatar: ( x, y ) => toDataURLForAvatar: ( x, y ) =>
return if @checkUrl() return if @checkUrl()
@resize( x * 2, y * 2 ) @resize( x, y, 2 )
@toDataURL( 'image/jpeg', 0.7 ) @toDataURL( 'image/jpeg', 0.7 )
toDataURLForApp: ( x, y ) => toDataURLForApp: ( x, y ) =>
return if @checkUrl() return if @checkUrl()
@resize( x * 2, y * 2 ) @resize( x, y, 2 )
@toDataURL( 'image/png', 0.7 ) @toDataURL( 'image/png', 0.7 )

View file

@ -58,24 +58,29 @@ class App.User extends App.Model
if placement if placement
placement = "data-placement=\"#{placement}\"" placement = "data-placement=\"#{placement}\""
if @image is 'none' if !@image || @image is 'none'
return @uniqueAvatar(size, placement, cssClass) return @uniqueAvatar(size, placement, cssClass)
else else
"<span class=\"avatar user-popover #{cssClass}\" data-id=\"#{@id}\" style=\"background-image: url(#{ @imageUrl })\" #{placement}></span>" "<span class=\"avatar user-popover #{cssClass}\" data-id=\"#{@id}\" style=\"background-image: url(#{ @imageUrl })\" #{placement}></span>"
uniqueAvatar: (size = 40, placement = '', cssClass = '') -> uniqueAvatar: (size = 40, placement = '', cssClass = '', avatar) ->
if size and !cssClass if size
cssClass += " size-#{ size }" cssClass += " size-#{ size }"
width = 300 width = 300
height = 226 height = 226
size = parseInt(size, 10) size = parseInt(size, 10)
rng = new Math.seedrandom(@id) rng = new Math.seedrandom(@id)
x = rng() * (width - size) x = rng() * (width - size)
y = rng() * (height - size) y = rng() * (height - size)
"<span class=\"avatar unique user-popover #{cssClass}\" data-id=\"#{@id}\" style=\"background-position: -#{ x }px -#{ y }px;\" #{placement}>#{ @initials() }</span>" if !avatar
cssClass += "#{cssClass} user-popover"
data = "data-id=\"#{@id}\""
else
data = "data-avatar-id=\"#{avatar.id}\""
"<span class=\"avatar unique #{cssClass}\" #{data} style=\"background-position: -#{ x }px -#{ y }px;\" #{placement}>#{ @initials() }</span>"
@_fillUp: (data) -> @_fillUp: (data) ->

View file

@ -1,4 +1,4 @@
<div class="avatar-holder"> <div class="avatar-holder">
<span class="avatar size-50" style="background-image: url(<%- @src %>)"></span> <span class="avatar size-50" data-avatar-id="<%- @avatar.id %>" style="background-image: url(<%- @src %>)"></span>
<div class="avatar-delete"><div class="delete icon"></div></div> <div class="avatar-delete"><div class="delete icon"></div></div>
</div> </div>

View file

@ -3,21 +3,27 @@
<h1><%- @T( 'Avatar' ) %></h1> <h1><%- @T( 'Avatar' ) %></h1>
<div class="page-header-meta"> <div class="page-header-meta">
<% if @webcamSupport: %> <% if @webcamSupport: %>
<div class="btn btn--success js-openCamera">Camera</div> <div class="btn btn--success js-openCamera"><%- @T('Camera') %></div>
<% end %> <% end %>
<div class="btn btn--success fileUpload">Upload<input type="file" class="js-upload" accept="image/*"></div> <div class="btn btn--success fileUpload"><%- @T('Upload') %><input type="file" class="js-upload" accept="image/*"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="avatar-gallery horizontal wrap"> <div class="avatar-gallery horizontal wrap">
<% for avatar in @avatars: %>
<div class="avatar-holder"> <div class="avatar-holder">
<%- App.Session.get().uniqueAvatar("50") %> <% if avatar.inital: %>
<% cssClass = '' %>
<% if avatar.default: %>
<% cssClass = 'is-active' %>
<% end %>
<%- App.Session.get().uniqueAvatar('50', '', cssClass, avatar) %>
<% else: %>
<span class="avatar size-50 <% if avatar.default: %>is-active<% end %>" data-avatar-id="<%- avatar.id %>" style="background-image: url(<%- avatar.content %>)"></span>
<% if avatar.deletable: %>
<div class="avatar-delete"><div class="delete icon"></div></div>
<% end %>
<% end %>
</div> </div>
<div class="avatar-holder"> <% end %>
<%- App.Session.get().avatar("50", undefined, 'is-active') %>
<div class="avatar-delete"><div class="delete icon"></div></div>
</div>
<!-- <div class="active">
<div class="size-50 avatar avatar--new centered"><div class="white plus icon"></div></div>
</div> -->
</div> </div>

View file

@ -190,29 +190,35 @@ class ApplicationController < ActionController::Base
end end
# return auth ok # return auth ok
return true true
end end
def is_role( role_name ) def is_role( role_name )
return false if !current_user return false if !current_user
return true if current_user.is_role( role_name ) return true if current_user.is_role( role_name )
return false false
end end
def ticket_permission(ticket) def ticket_permission(ticket)
return true if ticket.permission( :current_user => current_user ) return true if ticket.permission( :current_user => current_user )
response_access_deny response_access_deny
return false false
end end
def is_not_role( role_name ) def is_not_role( role_name )
deny_if_not_role( role_name ) deny_if_not_role( role_name )
end end
def deny_if_not_role( role_name ) def deny_if_not_role( role_name )
return false if is_role( role_name ) return false if is_role( role_name )
response_access_deny response_access_deny
return true true
end
def valid_session_with_user
return true if current_user
render :json => { :message => 'No session user!' }, :status => :unprocessable_entity
false
end end
def response_access_deny def response_access_deny
@ -220,7 +226,7 @@ class ApplicationController < ActionController::Base
:json => {}, :json => {},
:status => :unauthorized :status => :unauthorized
) )
return false false
end end
def config_frontend def config_frontend

View file

@ -71,7 +71,7 @@ curl http://localhost/api/v1/getting_started -v -u #{login}:#{password}
file = StaticAssets.data_url_attributes( params[:logo] ) file = StaticAssets.data_url_attributes( params[:logo] )
if !file[:content] || !file[:content_type] if !file[:content] || !file[:mime_type]
messages[:logo] = 'Unable to process image upload.' messages[:logo] = 'Unable to process image upload.'
end end
end end
@ -106,7 +106,7 @@ curl http://localhost/api/v1/getting_started -v -u #{login}:#{password}
file = StaticAssets.data_url_attributes( params[:logo] ) file = StaticAssets.data_url_attributes( params[:logo] )
# store image 1:1 # store image 1:1
StaticAssets.store_raw( file[:content], file[:content_type] ) StaticAssets.store_raw( file[:content], file[:mime_type] )
end end
if params[:logo_resize] && params[:logo_resize] =~ /^data:image/i if params[:logo_resize] && params[:logo_resize] =~ /^data:image/i
@ -115,7 +115,7 @@ curl http://localhost/api/v1/getting_started -v -u #{login}:#{password}
file = StaticAssets.data_url_attributes( params[:logo_resize] ) file = StaticAssets.data_url_attributes( params[:logo_resize] )
# store image 1:1 # store image 1:1
settings[:product_logo] = StaticAssets.store( file[:content], file[:content_type] ) settings[:product_logo] = StaticAssets.store( file[:content], file[:mime_type] )
end end
# set changed settings # set changed settings

View file

@ -155,7 +155,7 @@ class SessionsController < ApplicationController
current_user_set(authorization.user) current_user_set(authorization.user)
# log new session # log new session
user.activity_stream_log( 'session started', authorization.user.id, true ) authorization.user.activity_stream_log( 'session started', authorization.user.id, true )
# remember last login date # remember last login date
authorization.user.update_last_login authorization.user.update_last_login

View file

@ -95,12 +95,7 @@ curl http://localhost/api/v1/users/#{id}.json -v -u #{login}:#{password}
def show def show
# access deny # access deny
if is_role('Customer') && !is_role('Admin') && !is_role('Agent') return if !permission_check
if params[:id].to_i != current_user.id
response_access_deny
return
end
end
if params[:full] if params[:full]
full = User.full( params[:id] ) full = User.full( params[:id] )
@ -175,6 +170,10 @@ curl http://localhost/api/v1/users.json -v -u #{login}:#{password} -H "Content-T
# else do assignment as defined # else do assignment as defined
else else
# permission check by role
return if !permission_check_by_role
if params[:role_ids] if params[:role_ids]
user.role_ids = params[:role_ids] user.role_ids = params[:role_ids]
end end
@ -278,13 +277,8 @@ curl http://localhost/api/v1/users/2.json -v -u #{login}:#{password} -H "Content
def update def update
# allow user to update him self # access deny
if is_role('Customer') && !is_role('Admin') && !is_role('Agent') return if !permission_check
if params[:id] != current_user.id
response_access_deny
return
end
end
user = User.find( params[:id] ) user = User.find( params[:id] )
@ -606,23 +600,143 @@ curl http://localhost/api/v1/users/image/8d6cca1c6bdc226cf2ba131e264ca2c7 -v -u
def image def image
# cache image # cache image
response.headers['Expires'] = 1.year.from_now.httpdate response.headers['Expires'] = 1.year.from_now.httpdate
response.headers["Cache-Control"] = "cache, store, max-age=31536000, must-revalidate" response.headers['Cache-Control'] = 'cache, store, max-age=31536000, must-revalidate'
response.headers["Pragma"] = "cache" response.headers['Pragma'] = 'cache'
user = User.where( :image => params[:hash] ).first file = Avatar.get_by_hash( params[:hash] )
if user if file
image = user.get_image
send_data( send_data(
image[:content], file.content,
:filename => image[:filename], :filename => file.filename,
:type => image[:content_type], :type => file.preferences['Content-Type'] || file.preferences['Mime-Type'],
:disposition => 'inline' :disposition => 'inline'
) )
return return
end end
render :json => {}, :status => 404 # serve default image
image = 'R0lGODdhMAAwAOMAAMzMzJaWlr6+vqqqqqOjo8XFxbe3t7GxsZycnAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAMAAwAAAEcxDISau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru98TwuAA+KQAQqJK8EAgBAgMEqmkzUgBIeSwWGZtR5XhSqAULACCoGCJGwlm1MGQrq9RqgB8fm4ZTUgDBIEcRR9fz6HiImKi4yNjo+QkZKTlJWWkBEAOw=='
send_data(
Base64.decode64(image),
:filename => 'image.gif',
:type => 'image/gif',
:disposition => 'inline'
)
end
=begin
Resource:
POST /api/v1/users/avatar
Payload:
{
"avatar_full": "base64 url",
}
Response:
{
:message => 'ok'
}
Test:
curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"avatar": "base64 url"}'
=end
def avatar_new
return if !valid_session_with_user
# get & validate image
file_full = StaticAssets.data_url_attributes( params[:avatar_full] )
file_resize = StaticAssets.data_url_attributes( params[:avatar_resize] )
avatar = Avatar.add(
:object => 'User',
:o_id => current_user.id,
:full => {
:content => file_full[:content],
:mime_type => file_full[:mime_type],
},
:resize => {
:content => file_resize[:content],
:mime_type => file_resize[:mime_type],
},
:source => 'upload ' + Time.now.to_s,
:deletable => true,
)
# update user link
current_user.update_attributes( :image => avatar.store_hash )
render :json => { :avatar => avatar }, :status => :ok
end
def avatar_set_default
return if !valid_session_with_user
# get & validate image
if !params[:id]
render :json => { :message => 'No id of avatar!' }, :status => :unprocessable_entity
return
end
# set as default
avatar = Avatar.set_default( 'User', current_user.id, params[:id] )
# update user link
current_user.update_attributes( :image => avatar.store_hash )
render :json => {}, :status => :ok
end
def avatar_destroy
return if !valid_session_with_user
# get & validate image
if !params[:id]
render :json => { :message => 'No id of avatar!' }, :status => :unprocessable_entity
return
end
# remove avatar
Avatar.remove_one( 'User', current_user.id, params[:id] )
# update user link
avatar = Avatar.get_default( 'User', current_user.id )
current_user.update_attributes( :image => avatar.store_hash )
render :json => {}, :status => :ok
end
def avatar_list
return if !valid_session_with_user
# list of avatars
result = Avatar.list( 'User', current_user.id )
render :json => { :avatars => result }, :status => :ok
end
private
def permission_check_by_role
return true if is_role('Admin')
return true if is_role('Agent')
response_access_deny
return false
end
def permission_check
return true if is_role('Admin')
return true if is_role('Agent')
# allow to update customer by him self
return true if is_role('Customer') && params[:id].to_i == current_user.id
response_access_deny
return false
end end
end end

View file

@ -14,35 +14,60 @@ class Authorization < ApplicationModel
# update auth tokens # update auth tokens
auth.update_attributes( auth.update_attributes(
:token => hash['credentials']['token'], :token => hash['credentials']['token'],
:secret => hash['credentials']['secret'] :secret => hash['credentials']['secret']
) )
# update username of auth entry if empty # update username of auth entry if empty
if !auth.username && hash['info']['nickname'] if !auth.username && hash['info']['nickname']
auth.update_attributes( auth.update_attributes(
:username => hash['info']['nickname'], :username => hash['info']['nickname'],
) )
end end
# update image if needed # update image if needed
if hash['info']['image'] if hash['info']['image']
user = User.find( auth.user_id ) user = User.find( auth.user_id )
user.update_attributes(
:image_source => hash['info']['image'] # save/update avatar
avatar = Avatar.add(
:object => 'User',
:o_id => user.id,
:url => hash['info']['image'],
:source => hash['provider'],
:deletable => true,
:updated_by_id => user.id,
:created_by_id => user.id,
) )
# update user link
if avatar
user.update_column( :image, avatar.store_hash )
end
end end
end end
return auth auth
end end
def self.create_from_hash(hash, user = nil) def self.create_from_hash(hash, user = nil)
if user then if user
user.update_attributes(
# :username => hash['username'], # save/update avatar
:image_source => hash['info']['image'] avatar = Avatar.add(
:object => 'User',
:o_id => user.id,
:url => hash['info']['image'],
:source => hash['provider'],
:deletable => true,
:updated_by_id => user.id,
:created_by_id => user.id,
) )
# update user link
if avatar
user.update_column( :image, avatar.store_hash )
end
# fillup empty attributes # fillup empty attributes
# TODO # TODO
@ -50,7 +75,7 @@ class Authorization < ApplicationModel
user = User.create_from_hash!(hash) user = User.create_from_hash!(hash)
end end
auth = Authorization.create( Authorization.create(
:user => user, :user => user,
:uid => hash['uid'], :uid => hash['uid'],
:username => hash['info']['nickname'] || hash['username'], :username => hash['info']['nickname'] || hash['username'],
@ -58,12 +83,12 @@ class Authorization < ApplicationModel
:token => hash['credentials']['token'], :token => hash['credentials']['token'],
:secret => hash['credentials']['secret'] :secret => hash['credentials']['secret']
) )
return auth
end end
private private
def delete_user_cache
self.user.cache_delete def delete_user_cache
end self.user.cache_delete
end
end end

363
app/models/avatar.rb Normal file
View file

@ -0,0 +1,363 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
class Avatar < ApplicationModel
belongs_to :object_lookup, :class_name => 'ObjectLookup'
=begin
add a avatar based on auto detection (email address)
Avatar.auto_detection(
:object => 'User',
:o_id => user.id,
:url => 'somebody@example.com',
:source => 'web',
:updated_by_id => 1,
:created_by_id => 1,
)
=end
def self.auto_detection(data)
return if !data[:url]
return if data[:url].empty?
# dry gravatar lookup
hash = Digest::MD5.hexdigest(data[:url])
url = "http://www.gravatar.com/avatar/#{hash}.jpg?s=160&d=404"
puts "#{data[:url]}: #{url}"
Avatar.add(
:object => data[:object],
:o_id => data[:o_id],
:url => url,
:source => 'gravatar.com',
:deletable => false,
:updated_by_id => 1,
:created_by_id => 1,
)
end
=begin
add a avatar
Avatar.add(
:object => 'User',
:o_id => user.id,
:default => true,
:full => {
:content => '...',
:mime_type => 'image/png',
},
:resize => {
:content => '...',
:mime_type => 'image/png',
},
:source => 'web',
:updated_by_id => 1,
:created_by_id => 1,
)
=end
def self.add(data)
# lookups
if data[:object]
object_id = ObjectLookup.by_name( data[:object] )
end
# add inital avatar
add_init_avatar(object_id, data[:o_id])
record = {
:o_id => data[:o_id],
:object_lookup_id => object_id,
:default => true,
:deletable => data[:deletable],
:inital => false,
:source => data[:source],
:source_url => data[:url],
:updated_by_id => data[:updated_by_id],
:created_by_id => data[:created_by_id],
}
# check if avatar with url already exists
avatar_already_exists = nil
if data[:source] && !data[:source].empty?
avatar_already_exists = Avatar.where(
:object_lookup_id => object_id,
:o_id => data[:o_id],
:source => data[:source],
).first
end
# fetch image
if data[:url] && data[:url] =~ /^http/
# check if source ist already updated within last 2 minutes
if avatar_already_exists
if avatar_already_exists.source_url == data[:url]
if avatar_already_exists.updated_at > 2.minutes.ago
return
end
end
end
# twitter workaround to get bigger avatar images
# see also https://dev.twitter.com/overview/general/user-profile-images-and-banners
if data[:url] =~ /\/\/pbs.twimg.com\//i
data[:url].sub!(/normal\.png$/, 'bigger.png')
end
# fetch image
response = UserAgent.request( data[:url] )
if !response.success?
#puts "WARNING: Can't fetch '#{self.image_source}' (maybe no avatar available), http code: #{response.code.to_s}"
#raise "Can't fetch '#{self.image_source}', http code: #{response.code.to_s}"
return
end
#puts "NOTICE: Fetch '#{self.image_source}', http code: #{response.code.to_s}"
mime_type = 'image'
if data[:url] =~ /\.png/i
mime_type = 'image/png'
end
if data[:url] =~ /\.(jpg|jpeg)/i
mime_type = 'image/png'
end
if !data[:resize]
data[:resize] = {}
end
data[:resize][:content] = response.body
data[:resize][:mime_type] = mime_type
if !data[:full]
data[:full] = {}
end
data[:full][:content] = response.body
data[:full][:mime_type] = mime_type
end
# check if avatar need to be updated
record[:store_hash] = Digest::MD5.hexdigest( data[:resize][:content] )
if avatar_already_exists
return if avatar_already_exists.store_hash == record[:store_hash]
end
# store images
object_name = "Avatar::#{data[:object]}"
if data[:full]
store_full = Store.add(
:object => "#{object_name}::Full",
:o_id => data[:o_id],
:data => data[:full][:content],
:filename => 'avatar_full',
:preferences => {
'Mime-Type' => data[:full][:mime_type]
},
:created_by_id => data[:created_by_id],
)
record[:store_full_id] = store_full.id
record[:store_hash] = Digest::MD5.hexdigest( data[:full][:content] )
end
if data[:resize]
store_resize = Store.add(
:object => "#{object_name}::Resize",
:o_id => data[:o_id],
:data => data[:resize][:content],
:filename => 'avatar',
:preferences => {
'Mime-Type' => data[:resize][:mime_type]
},
:created_by_id => data[:created_by_id],
)
record[:store_resize_id] = store_resize.id
record[:store_hash] = Digest::MD5.hexdigest( data[:resize][:content] )
end
# update existing
if avatar_already_exists
avatar_already_exists.update_attributes( record )
avatar = avatar_already_exists
# add new one and set it as default
else
avatar = Avatar.create(record)
set_default_items(object_id, data[:o_id], avatar.id)
end
avatar
end
=begin
set avatars as default
Avatar.set_default( 'User', 123, avatar_id )
=end
def self.set_default( object_name, o_id, avatar_id )
object_id = ObjectLookup.by_name( object_name )
avatar = Avatar.where(
:object_lookup_id => object_id,
:o_id => o_id,
:id => avatar_id,
).first
avatar.default = true
avatar.save!
# set all other to default false
set_default_items(object_id, o_id, avatar_id)
avatar
end
=begin
remove all avatars of an object
Avatar.remove( 'User', 123 )
=end
def self.remove( object_name, o_id )
object_id = ObjectLookup.by_name( object_name )
Avatar.where(
:object_lookup_id => object_id,
:o_id => o_id,
).destroy_all
object_name_store = "Avatar::#{object_name}"
Store.remove(
:object => "#{object_name_store}::Full",
:o_id => o_id,
)
Store.remove(
:object => "#{object_name_store}::Resize",
:o_id => o_id,
)
end
=begin
remove one avatars of an object
Avatar.remove_one( 'User', 123, avatar_id )
=end
def self.remove_one( object_name, o_id, avatar_id )
object_id = ObjectLookup.by_name( object_name )
Avatar.where(
:object_lookup_id => object_id,
:o_id => o_id,
:id => avatar_id,
).destroy_all
end
=begin
return all avatars of an user
avatars = Avatar.list( 'User', 123 )
=end
def self.list(object_name, o_id)
object_id = ObjectLookup.by_name( object_name )
avatars = Avatar.where(
:object_lookup_id => object_id,
:o_id => o_id,
).order( 'inital DESC, deletable ASC, created_at ASC, id DESC' )
# add inital avatar
add_init_avatar(object_id, o_id)
avatar_list = []
avatars.each do |avatar|
data = avatar.attributes
if avatar.store_resize_id
file = Store.find(avatar.store_resize_id)
data['content'] = "data:#{ file.preferences['Mime-Type'] };base64,#{ Base64.strict_encode64( file.content ) }"
end
avatar_list.push data
end
avatar_list
end
=begin
get default avatar image of user by hash
store = Avatar.get_by_hash( hash )
returns:
store object
=end
def self.get_by_hash(hash)
avatar = Avatar.where(
:store_hash => hash,
).first
return if !avatar
file = Store.find(avatar.store_resize_id)
end
=begin
get default avatar of user by user id
avatar = Avatar.get_default( 'User', user_id )
returns:
avatar object
=end
def self.get_default(object_name, o_id)
object_id = ObjectLookup.by_name( object_name )
Avatar.where(
:object_lookup_id => object_id,
:o_id => o_id,
:default => true,
).first
end
private
def self.set_default_items(object_id, o_id, avatar_id)
avatars = Avatar.where(
:object_lookup_id => object_id,
:o_id => o_id,
).order( 'created_at ASC, id DESC' )
avatars.each do |avatar|
next if avatar.id == avatar_id
avatar.default = false
avatar.save!
end
end
def self.add_init_avatar(object_id, o_id)
count = Avatar.where(
:object_lookup_id => object_id,
:o_id => o_id,
).count
return if count > 0
Avatar.create(
:o_id => o_id,
:object_lookup_id => object_id,
:default => true,
:source => 'init',
:inital => true,
:deletable => false,
:updated_by_id => 1,
:created_by_id => 1,
)
end
end

View file

@ -8,11 +8,11 @@ class User < ApplicationModel
extend User::Search extend User::Search
include User::SearchIndex include User::SearchIndex
before_create :check_name, :check_email, :check_login, :check_image, :check_password before_create :check_name, :check_email, :check_login, :check_password
before_update :check_password, :check_image, :check_email, :check_login_update before_update :check_password, :check_email
after_create :check_image_load, :notify_clients_after_create after_create :avatar_check, :notify_clients_after_create
after_update :check_image_load, :notify_clients_after_update after_update :avatar_check, :notify_clients_after_update
after_destroy :notify_clients_after_destroy after_destroy :avatar_destroy, :notify_clients_after_destroy
has_and_belongs_to_many :groups, :after_add => :cache_update, :after_remove => :cache_update has_and_belongs_to_many :groups, :after_add => :cache_update, :after_remove => :cache_update
has_and_belongs_to_many :roles, :after_add => :cache_update, :after_remove => :cache_update has_and_belongs_to_many :roles, :after_add => :cache_update, :after_remove => :cache_update
@ -382,48 +382,6 @@ returns
self.save self.save
end end
=begin
get image of user
user = User.find(123)
result = user.get_image
returns
result = {
:filename => 'some filename',
:content_type => 'image/png',
:content => bin_string,
}
=end
def get_image
# find file
list = Store.list( :object => 'User::Image', :o_id => self.id )
logger.debug list.inspect
if list && list[0]
file = Store.find( list[0] )
result = {
:content => file.content,
:filename => file.filename,
:content_type => file.preferences['Content-Type'] || file.preferences['Mime-Type'],
}
return result
end
# serve default image
image = 'R0lGODdhMAAwAOMAAMzMzJaWlr6+vqqqqqOjo8XFxbe3t7GxsZycnAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAMAAwAAAEcxDISau9OOvNu/9gKI5kaZ5oqq5s675wLM90bd94ru98TwuAA+KQAQqJK8EAgBAgMEqmkzUgBIeSwWGZtR5XhSqAULACCoGCJGwlm1MGQrq9RqgB8fm4ZTUgDBIEcRR9fz6HiImKi4yNjo+QkZKTlJWWkBEAOw=='
result = {
:content => Base64.decode64(image),
:filename => 'image.gif',
:content_type => 'image/gif',
}
result
end
private private
def check_name def check_name
@ -472,7 +430,7 @@ returns
while check while check
exists = User.where( :login => self.login ).first exists = User.where( :login => self.login ).first
if exists if exists
self.login = self.login + rand(99).to_s self.login = self.login + rand(999).to_s
else else
check = false check = false
end end
@ -480,59 +438,30 @@ returns
end end
end end
# FIXME: Remove me later def avatar_check
def check_login_update
if self.login
self.login = self.login.downcase
end
end
def check_image return if !self.email
if !self.image_source || self.image_source == '' || self.image_source =~ /gravatar.com/i return if self.email.empty?
if self.email
hash = Digest::MD5.hexdigest(self.email)
self.image_source = "http://www.gravatar.com/avatar/#{hash}?s=160&d=404"
logger.debug "#{self.email}: #{self.image_source}"
end
end
end
def check_image_load # save/update avatar
avatar = Avatar.auto_detection(
return if !self.image_source :object => 'User',
return if self.image_source !~ /http/i :o_id => self.id,
:url => self.email,
# download image :source => 'app',
response = UserAgent.request( self.image_source ) :updated_by_id => self.updated_by_id,
if !response.success?
self.update_column( :image, 'none' )
self.cache_delete
#puts "WARNING: Can't fetch '#{self.image_source}' (maybe no avatar available), http code: #{response.code.to_s}"
#raise "Can't fetch '#{self.image_source}', http code: #{response.code.to_s}"
return
end
#puts "NOTICE: Fetch '#{self.image_source}', http code: #{response.code.to_s}"
# store image local
hash = Digest::MD5.hexdigest( response.body )
# check if image has changed
return if self.image == hash
#puts "NOTICE: update image in store"
# save new image
self.update_column( :image, hash )
Store.remove( :object => 'User::Image', :o_id => self.id )
Store.add(
:object => 'User::Image',
:o_id => self.id,
:data => response.body,
:filename => 'image',
:preferences => {
'Content-Type' => response.content_type
},
:created_by_id => self.updated_by_id, :created_by_id => self.updated_by_id,
) )
self.cache_delete
# update user link
if avatar
self.update_column( :image, avatar.store_hash )
self.cache_delete
end
end
def avatar_destroy
Avatar.remove( 'User', self.id )
end end
def check_password def check_password
@ -542,16 +471,17 @@ returns
# get current record # get current record
if self.id if self.id
current = User.find(self.id) #current = User.find(self.id)
self.password = current.password #self.password = current.password
self.password = self.password_was
end end
# create crypted password if not already crypted end
else
if self.password !~ /^\{sha2\}/ # crypt password if not already crypted
crypted = Digest::SHA2.hexdigest( self.password ) if self.password && self.password !~ /^\{sha2\}/
self.password = "{sha2}#{crypted}" crypted = Digest::SHA2.hexdigest( self.password )
end self.password = "{sha2}#{crypted}"
end end
end end
end end

View file

@ -8,6 +8,12 @@ Zammad::Application.routes.draw do
match api_path + '/users/password_change', :to => 'users#password_change', :via => :post match api_path + '/users/password_change', :to => 'users#password_change', :via => :post
match api_path + '/users/preferences', :to => 'users#preferences', :via => :put match api_path + '/users/preferences', :to => 'users#preferences', :via => :put
match api_path + '/users/account', :to => 'users#account_remove', :via => :delete match api_path + '/users/account', :to => 'users#account_remove', :via => :delete
match api_path + '/users/avatar', :to => 'users#avatar_new', :via => :post
match api_path + '/users/avatar', :to => 'users#avatar_list', :via => :get
match api_path + '/users/avatar', :to => 'users#avatar_destroy', :via => :delete
match api_path + '/users/avatar/set', :to => 'users#avatar_set_default', :via => :post
match api_path + '/users', :to => 'users#index', :via => :get match api_path + '/users', :to => 'users#index', :via => :get
match api_path + '/users/:id', :to => 'users#show', :via => :get match api_path + '/users/:id', :to => 'users#show', :via => :get
match api_path + '/users/history/:id', :to => 'users#history', :via => :get match api_path + '/users/history/:id', :to => 'users#history', :via => :get

View file

@ -0,0 +1,27 @@
class CreateAvatar < ActiveRecord::Migration
def up
create_table :avatars do |t|
t.column :o_id, :integer, :null => false
t.column :object_lookup_id, :integer, :null => false
t.column :default, :boolean, :null => false, :default => false
t.column :deletable, :boolean, :null => false, :default => true
t.column :inital, :boolean, :null => false, :default => false
t.column :store_full_id, :integer, :null => true
t.column :store_resize_id, :integer, :null => true
t.column :store_hash, :string, :limit => 32, :null => true
t.column :source, :string, :limit => 100, :null => false
t.column :source_url, :string, :limit => 512, :null => true
t.column :updated_by_id, :integer, :null => false
t.column :created_by_id, :integer, :null => false
t.timestamps
end
add_index :avatars, [:o_id, :object_lookup_id]
add_index :avatars, [:store_hash]
add_index :avatars, [:source]
add_index :avatars, [:source_url]
add_index :avatars, [:default]
end
def down
end
end

View file

@ -3,8 +3,8 @@ module StaticAssets
def self.data_url_attributes( data_url ) def self.data_url_attributes( data_url )
data = {} data = {}
if data_url =~ /^data:(.+?);base64,(.+?)$/ if data_url =~ /^data:(.+?);base64,(.+?)$/
data[:content_type] = $1 data[:mime_type] = $1
data[:content] = Base64.decode64($2) data[:content] = Base64.decode64($2)
return data return data
end end
raise "Unable to parse data url: #{data_url.substr(0,100)}" raise "Unable to parse data url: #{data_url.substr(0,100)}"

View file

@ -17,7 +17,7 @@ class UserTest < ActiveSupport::TestCase
:create_verify => { :create_verify => {
:firstname => 'Firstname', :firstname => 'Firstname',
:lastname => 'Lastname', :lastname => 'Lastname',
:image => 'none', :image => nil,
:email => 'some@example.com', :email => 'some@example.com',
:login => 'some@example.com', :login => 'some@example.com',
}, },
@ -35,7 +35,7 @@ class UserTest < ActiveSupport::TestCase
:create_verify => { :create_verify => {
:firstname => 'Firstname', :firstname => 'Firstname',
:lastname => 'Lastname', :lastname => 'Lastname',
:image => 'none', :image => nil,
:email => 'some@example.com', :email => 'some@example.com',
:login => 'some@example.com', :login => 'some@example.com',
}, },
@ -53,7 +53,7 @@ class UserTest < ActiveSupport::TestCase
:create_verify => { :create_verify => {
:firstname => 'Firstname', :firstname => 'Firstname',
:lastname => 'Lastname', :lastname => 'Lastname',
:image => 'none', :image => nil,
:email => 'some@example.com', :email => 'some@example.com',
:login => 'some@example.com', :login => 'some@example.com',
}, },
@ -139,8 +139,7 @@ class UserTest < ActiveSupport::TestCase
:create_verify => { :create_verify => {
:firstname => 'Bob', :firstname => 'Bob',
:lastname => 'Smith', :lastname => 'Smith',
:image => 'none', :image => nil,
:image_md5 => '76fdc28c07e4f3d7802b75aacfccdf6a',
:email => 'bob.smith@example.com', :email => 'bob.smith@example.com',
:login => 'login-4', :login => 'login-4',
}, },
@ -203,8 +202,8 @@ class UserTest < ActiveSupport::TestCase
assert_equal( value, user[key], "create check #{ key } in (#{ test[:name] })" ) assert_equal( value, user[key], "create check #{ key } in (#{ test[:name] })" )
} }
if test[:create_verify][:image_md5] if test[:create_verify][:image_md5]
file = user.get_image file = Avatar.get_by_hash( user.image )
file_md5 = Digest::MD5.hexdigest( file[:content] ) file_md5 = Digest::MD5.hexdigest( file.content )
assert_equal( test[:create_verify][:image_md5], file_md5, "create avatar md5 check in (#{ test[:name] })" ) assert_equal( test[:create_verify][:image_md5], file_md5, "create avatar md5 check in (#{ test[:name] })" )
end end
if test[:update] if test[:update]
@ -216,8 +215,8 @@ class UserTest < ActiveSupport::TestCase
} }
if test[:update_verify][:image_md5] if test[:update_verify][:image_md5]
file = user.get_image file = Avatar.get_by_hash( user.image )
file_md5 = Digest::MD5.hexdigest( file[:content] ) file_md5 = Digest::MD5.hexdigest( file.content )
assert_equal( test[:update_verify][:image_md5], file_md5, "update avatar md5 check in (#{ test[:name] })" ) assert_equal( test[:update_verify][:image_md5], file_md5, "update avatar md5 check in (#{ test[:name] })" )
end end
end end