]> BookStack Code Mirror - bookstack/commitdiff
Added custom user avatars
authorDan Brown <redacted>
Wed, 9 Dec 2015 22:30:55 +0000 (22:30 +0000)
committerDan Brown <redacted>
Wed, 9 Dec 2015 22:30:55 +0000 (22:30 +0000)
16 files changed:
.env.example
app/Http/Controllers/ImageController.php
app/Http/Controllers/UserController.php
app/Http/routes.php
app/Image.php
app/Repos/ImageRepo.php
app/Services/ImageService.php
app/User.php
database/migrations/2015_12_09_195748_add_user_avatars.php [new file with mode: 0644]
public/user_avatar.png [new file with mode: 0644]
resources/assets/js/components/image-manager.vue
resources/assets/js/components/image-picker.vue
resources/assets/sass/styles.scss
resources/views/settings/index.blade.php
resources/views/users/edit.blade.php
resources/views/users/form.blade.php

index 6d9189b5d5cb255f5350a53a7837ff5439a8c815..91e59f9664c06d932cfce3dc70ca2bcc0fd86fe9 100644 (file)
@@ -33,6 +33,9 @@ GOOGLE_APP_SECRET=false
 # URL used for social login redirects, NO TRAILING SLASH
 APP_URL=http://bookstack.dev
 
+# External services
+USE_GRAVATAR=true
+
 # Mail settings
 MAIL_DRIVER=smtp
 MAIL_HOST=localhost
index 23f5446d65982ff0acfc8b1b5c2b6039ed75b5b9..146dd0c05e8ef6988b13cf65ab833a607b24c120 100644 (file)
@@ -33,7 +33,7 @@ class ImageController extends Controller
 
 
     /**
-     * Get all gallery images, Paginated
+     * Get all images for a specific type, Paginated
      * @param int $page
      * @return \Illuminate\Http\JsonResponse
      */
@@ -43,6 +43,17 @@ class ImageController extends Controller
         return response()->json($imgData);
     }
 
+    /**
+     * Get all images for a user.
+     * @param int $page
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function getAllForUserType($page = 0)
+    {
+        $imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $this->currentUser->id);
+        return response()->json($imgData);
+    }
+
 
     /**
      * Handles image uploads for use on pages.
index d1c328b86a9a01e03798d8d6c2d43bf12dea4444..c3f08a2f85109d208eec055210a3eb8d929baa12 100644 (file)
@@ -62,7 +62,7 @@ class UserController extends Controller
         $this->checkPermission('user-create');
         $this->validate($request, [
             'name'             => 'required',
-            'email'            => 'required|email',
+            'email'            => 'required|email|unique:users,email',
             'password'         => 'required|min:5',
             'password-confirm' => 'required|same:password',
             'role'             => 'required|exists:roles,id'
index 274fccbff7e55a64150c5e12719b656a301fcc53..23d4c33ab6cc876c233a40e1e66fb0f08fe61d64 100644 (file)
@@ -57,6 +57,9 @@ Route::group(['middleware' => 'auth'], function () {
 
     // Image routes
     Route::group(['prefix' => 'images'], function() {
+        // Get for user images
+        Route::get('/user/all', 'ImageController@getAllForUserType');
+        Route::get('/user/all/{page}', 'ImageController@getAllForUserType');
         // Standard get, update and deletion for all types
         Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail');
         Route::put('/update/{imageId}', 'ImageController@update');
index 66d54ba302befb6c9df5f232be5a262dcce4a08f..651c618e6203b5dfd3cfd9b63286824b178de580 100644 (file)
@@ -3,9 +3,10 @@
 namespace BookStack;
 
 
+use Illuminate\Database\Eloquent\Model;
 use Images;
 
-class Image
+class Image extends Model
 {
     use Ownable;
 
@@ -16,9 +17,10 @@ class Image
      * @param  int       $width
      * @param  int       $height
      * @param bool|false $hardCrop
+     * @return string
      */
     public function getThumb($width, $height, $hardCrop = false)
     {
-        Images::getThumbnail($this, $width, $height, $hardCrop);
+        return Images::getThumbnail($this, $width, $height, $hardCrop);
     }
 }
index 56b0ba98d141be86ed80a25599b35ba18ac23366..d41909ac5e06a0626155e6857aa117898387c818 100644 (file)
@@ -17,7 +17,7 @@ class ImageRepo
      * @param Image        $image
      * @param ImageService $imageService
      */
-    public function __construct(Image $image,ImageService $imageService)
+    public function __construct(Image $image, ImageService $imageService)
     {
         $this->image = $image;
         $this->imageService = $imageService;
@@ -40,12 +40,18 @@ class ImageRepo
      * @param string $type
      * @param int    $page
      * @param int    $pageSize
+     * @param bool|int   $userFilter
      * @return array
      */
-    public function getPaginatedByType($type, $page = 0, $pageSize = 24)
+    public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
     {
-        $images = $this->image->where('type', '=', strtolower($type))
-            ->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
+        $images = $this->image->where('type', '=', strtolower($type));
+
+        if ($userFilter !== false) {
+            $images = $images->where('created_by', '=', $userFilter);
+        }
+
+        $images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
         $hasMore = count($images) > $pageSize;
 
         $returnImages = $images->take(24);
@@ -54,7 +60,7 @@ class ImageRepo
         });
 
         return [
-            'images' => $returnImages,
+            'images'  => $returnImages,
             'hasMore' => $hasMore
         ];
     }
@@ -67,7 +73,7 @@ class ImageRepo
      */
     public function saveNew(UploadedFile $uploadFile, $type)
     {
-        $image = $this->imageService->saveNew($this->image, $uploadFile, $type);
+        $image = $this->imageService->saveNewFromUpload($uploadFile, $type);
         $this->loadThumbs($image);
         return $image;
     }
index e6ee4cf0be3f083d6e2e41b6c9b02b97137b738b..57293209c5f5969842009de26c0764812b6d5049 100644 (file)
@@ -1,6 +1,7 @@
 <?php namespace BookStack\Services;
 
 use BookStack\Image;
+use BookStack\User;
 use Intervention\Image\ImageManager;
 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
@@ -34,11 +35,48 @@ class ImageService
         $this->cache = $cache;
     }
 
-    public function saveNew(Image $image, UploadedFile $uploadedFile, $type)
+    /**
+     * Saves a new image from an upload.
+     * @param UploadedFile $uploadedFile
+     * @param  string      $type
+     * @return mixed
+     */
+    public function saveNewFromUpload(UploadedFile $uploadedFile, $type)
+    {
+        $imageName = $uploadedFile->getClientOriginalName();
+        $imageData = file_get_contents($uploadedFile->getRealPath());
+        return $this->saveNew($imageName, $imageData, $type);
+    }
+
+
+    /**
+     * Gets an image from url and saves it to the database.
+     * @param             $url
+     * @param string      $type
+     * @param bool|string $imageName
+     * @return mixed
+     * @throws \Exception
+     */
+    private function saveNewFromUrl($url, $type, $imageName = false)
+    {
+        $imageName = $imageName ? $imageName : basename($url);
+        $imageData = file_get_contents($url);
+        if($imageData === false) throw new \Exception('Cannot get image from ' . $url);
+        return $this->saveNew($imageName, $imageData, $type);
+    }
+
+    /**
+     * Saves a new image
+     * @param string $imageName
+     * @param string $imageData
+     * @param string $type
+     * @return Image
+     */
+    private function saveNew($imageName, $imageData, $type)
     {
         $storage = $this->getStorage();
         $secureUploads = Setting::get('app-secure-images');
-        $imageName = str_replace(' ', '-', $uploadedFile->getClientOriginalName());
+        $imageName = str_replace(' ', '-', $imageName);
 
         if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
 
@@ -48,14 +86,14 @@ class ImageService
         }
         $fullPath = $imagePath . $imageName;
 
-        $storage->put($fullPath, file_get_contents($uploadedFile->getRealPath()));
+        $storage->put($fullPath, $imageData);
 
         $userId = auth()->user()->id;
-        $image = $image->forceCreate([
-            'name' => $imageName,
-            'path' => $fullPath,
-            'url' => $this->getPublicUrl($fullPath),
-            'type' => $type,
+        $image = Image::forceCreate([
+            'name'       => $imageName,
+            'path'       => $fullPath,
+            'url'        => $this->getPublicUrl($fullPath),
+            'type'       => $type,
             'created_by' => $userId,
             'updated_by' => $userId
         ]);
@@ -137,6 +175,26 @@ class ImageService
         return true;
     }
 
+    /**
+     * Save a gravatar image and set a the profile image for a user.
+     * @param User $user
+     * @param int  $size
+     * @return mixed
+     */
+    public function saveUserGravatar(User $user, $size = 500)
+    {
+        if (!env('USE_GRAVATAR', false)) return false;
+        $emailHash = md5(strtolower(trim($user->email)));
+        $url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
+        $imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
+        $image = $this->saveNewFromUrl($url, 'user', $imageName);
+        $image->created_by = $user->id;
+        $image->save();
+        $user->avatar()->associate($image);
+        $user->save();
+        return $image;
+    }
+
     /**
      * Get the storage that will be used for storing images.
      * @return FileSystemInstance
index 570789f37423d9f9ab8cdf7c5c82ad932ac2c42b..bf2b14ac409ec1cee2cc7db647c009b71f0d154c 100644 (file)
@@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      *
      * @var array
      */
-    protected $fillable = ['name', 'email', 'password'];
+    protected $fillable = ['name', 'email', 'password', 'image_id'];
 
     /**
      * The attributes excluded from the model's JSON form.
@@ -145,8 +145,17 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     public function getAvatar($size = 50)
     {
-        $emailHash = md5(strtolower(trim($this->email)));
-        return '//www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
+        if ($this->image_id === 0 || $this->image_id === null) return '/user_avatar.png';
+        return $this->avatar->getThumb($size, $size, true);
+    }
+
+    /**
+     * Get the avatar for the user.
+     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+     */
+    public function avatar()
+    {
+        return $this->belongsTo('BookStack\Image', 'image_id');
     }
 
     /**
diff --git a/database/migrations/2015_12_09_195748_add_user_avatars.php b/database/migrations/2015_12_09_195748_add_user_avatars.php
new file mode 100644 (file)
index 0000000..47cb027
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class AddUserAvatars extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->integer('image_id')->default(0);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->dropColumn('image_id');
+        });
+    }
+}
diff --git a/public/user_avatar.png b/public/user_avatar.png
new file mode 100644 (file)
index 0000000..26440a9
Binary files /dev/null and b/public/user_avatar.png differ
index c47fee184dac95ae79696dbe19e823a997d10528..dd4a77d57855420390dd0ccb00390839e023767b 100644 (file)
             imageType: {
                 type: String,
                 required: true
-            },
-            resizeWidth: {
-                type: String
-            },
-            resizeHeight: {
-                type: String
-            },
-            resizeCrop: {
-                type: Boolean
             }
         },
 
             },
 
             returnCallback: function (image) {
-                var _this = this;
-                var isResized = _this.resizeWidth && _this.resizeHeight;
-
-                if (!isResized) {
-                    _this.callback(image);
-                    return;
-                }
-
-                var cropped = _this.resizeCrop ? 'true' : 'false';
-                var requestString = '/images/thumb/' + image.id + '/' + _this.resizeWidth + '/' + _this.resizeHeight + '/' + cropped;
-                _this.$http.get(requestString, function(data) {
-                    image.thumbs.custom = data.url;
-                    _this.callback(image);
-                });
-
+                this.callback(image);
             },
 
             imageClick: function (image) {
index 4318ea1f08269860f13487bcc0f374761ab12bdb..7be976caf409298c4e83144f5e7d9436d9497db0 100644 (file)
@@ -7,31 +7,89 @@
         </div>
         <button class="button" type="button" @click="showImageManager">Select Image</button>
         <br>
-        <button class="text-button" @click="reset" type="button">Reset</button> <span class="sep">|</span> <button class="text-button neg" v-on:click="remove" type="button">Remove</button>
-        <input type="hidden" :name="name" :id="name" v-model="image">
+        <button class="text-button" @click="reset" type="button">Reset</button> <span v-show="showRemove" class="sep">|</span> <button v-show="showRemove" class="text-button neg" @click="remove" type="button">Remove</button>
+        <input type="hidden" :name="name" :id="name" v-model="value">
     </div>
 </template>
 
 <script>
     module.exports = {
-        props: ['currentImage', 'name', 'imageClass', 'defaultImage'],
+        props: {
+            currentImage: {
+                required: true,
+                type: String
+            },
+            currentId: {
+                required: false,
+                default: 'false',
+                type: String
+            },
+            name: {
+                required: true,
+                type: String
+            },
+            defaultImage: {
+                required: true,
+                type: String
+            },
+            imageClass: {
+                required: true,
+                type: String
+            },
+            resizeWidth: {
+                type: String
+            },
+            resizeHeight: {
+                type: String
+            },
+            resizeCrop: {
+                type: Boolean
+            },
+            showRemove: {
+                type: Boolean,
+                default: 'true'
+            }
+        },
         data: function() {
             return {
-                image: this.currentImage
+                image: this.currentImage,
+                value: false
             }
         },
+        compiled: function() {
+            this.value = this.currentId === 'false' ? this.currentImage : this.currentId;
+        },
         methods: {
+            setCurrentValue: function(imageModel, imageUrl) {
+                this.image = imageUrl;
+                this.value = this.currentId === 'false' ?  imageUrl : imageModel.id;
+            },
             showImageManager: function(e) {
                 var _this = this;
                 ImageManager.show(function(image) {
-                    _this.image = image.thumbs.custom || image.url;
+                    _this.updateImageFromModel(image);
                 });
             },
             reset: function() {
-                this.image = '';
+                this.setCurrentValue({id: 0}, this.defaultImage);
             },
             remove: function() {
                 this.image = 'none';
+            },
+            updateImageFromModel: function(model) {
+                var _this = this;
+                var isResized = _this.resizeWidth && _this.resizeHeight;
+
+                if (!isResized) {
+                    _this.setCurrentValue(model, model.url);
+                    return;
+                }
+
+                var cropped = _this.resizeCrop ? 'true' : 'false';
+                var requestString = '/images/thumb/' + model.id + '/' + _this.resizeWidth + '/' + _this.resizeHeight + '/' + cropped;
+                _this.$http.get(requestString, function(data) {
+                    _this.setCurrentValue(model, data.url);
+                });
             }
         }
     };
index 43bd735fd5c6281c46c9f25ae7dd43d82e38f54b..174c90669e010dbcc8061ad42aef29882d97f711 100644 (file)
@@ -36,6 +36,10 @@ body.dragging, body.dragging * {
     width: 40px;
     height: 40px;
   }
+  &.large {
+    width: 80px;
+    height: 80px;
+  }
 }
 
 // System wide notifications
index 759bb16786d7e727ae5c36e65f0aef5ce321b4a7..d1db1ed331490e34339192aef450f0e091b533d6 100644 (file)
@@ -33,7 +33,7 @@
                 <div class="form-group" id="logo-control">
                     <label for="setting-app-logo">Application Logo</label>
                     <p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p>
-                    <image-picker current-image="{{ Setting::get('app-logo', '') }}" default-image="/logo.png" name="setting-app-logo" image-class="logo-image"></image-picker>
+                    <image-picker resize-height="43" resize-width="200" current-image="{{ Setting::get('app-logo', '') }}" default-image="/logo.png" name="setting-app-logo" image-class="logo-image"></image-picker>
                 </div>
             </div>
         </div>
@@ -86,6 +86,6 @@
 
 </div>
 
-<image-manager image-type="system" resize-height="43" resize-width="200"></image-manager>
+<image-manager image-type="system"></image-manager>
 
 @stop
index 5e28059fedf62b4d8a596cbfa1966cbf1ec85e07..2a2167279d9c5e1ad87b02224fbc5929b6e47d0d 100644 (file)
 
 
     <div class="container small">
-
+        <form action="/users/{{$user->id}}" method="post">
         <div class="row">
             <div class="col-md-6">
                 <h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1>
-                <form action="/users/{{$user->id}}" method="post">
-                    {!! csrf_field() !!}
-                    <input type="hidden" name="_method" value="put">
-                    @include('users/form', ['model' => $user])
-                </form>
+                {!! csrf_field() !!}
+                <input type="hidden" name="_method" value="put">
+                @include('users/form', ['model' => $user])
+
             </div>
             <div class="col-md-6">
                 <h1>&nbsp;</h1>
-                <div class="shaded padded margin-top">
-                    <p>
-                        <img class="avatar" src="{{ $user->getAvatar(80) }}" alt="{{ $user->name }}">
-                    </p>
-                    <p class="text-muted">You can change your profile picture at <a href="http://en.gravatar.com/">Gravatar</a>.</p>
+                <div class="form-group" id="logo-control">
+                    <label for="user-avatar">User Avatar</label>
+                    <p class="small">This image should be approx 256px square.</p>
+                    <image-picker resize-height="512" resize-width="512" current-image="{{ $user->getAvatar(80) }}" current-id="{{ $user->image_id }}" default-image="/user_avatar.png" name="image_id" show-remove="false" image-class="avatar large"></image-picker>
                 </div>
             </div>
         </div>
+        </form>
 
         <hr class="margin-top large">
 
@@ -80,5 +79,5 @@
     </div>
 
     <p class="margin-top large"><br></p>
-
+    <image-manager image-type="user"></image-manager>
 @stop
index a8aa7e63fae84c66ace4f5e30bbf9131135733f6..16176bb8decfa0a7fa24877c7809f8183d44d9f2 100644 (file)
@@ -36,4 +36,5 @@
 <div class="form-group">
     <a href="/users" class="button muted">Cancel</a>
     <button class="button pos" type="submit">Save</button>
-</div>
\ No newline at end of file
+</div>
+