diff --git a/app/Commands/RenameGenreCommand.php b/app/Commands/RenameGenreCommand.php new file mode 100644 index 00000000..f4e99005 --- /dev/null +++ b/app/Commands/RenameGenreCommand.php @@ -0,0 +1,77 @@ +. + */ + +namespace Poniverse\Ponyfm\Commands; + +use Gate; +use Illuminate\Support\Str; +use Poniverse\Ponyfm\Genre; +use Validator; + +class RenameGenreCommand extends CommandBase +{ + /** @var Genre */ + private $_genre; + private $_newName; + + public function __construct($genreId, $newName) + { + $this->_genre = Genre::find($genreId);; + $this->_newName = $newName; + } + + /** + * @return bool + */ + public function authorize() + { + return Gate::allows('rename', $this->_genre); + } + + /** + * @throws \Exception + * @return CommandResponse + */ + public function execute() + { + $slug = Str::slug($this->_newName); + + $rules = [ + 'name' => 'required|unique:genres,name,'.$this->_genre->id.'|max:50', + 'slug' => 'required|unique:genres,slug,'.$this->_genre->id + ]; + + $validator = Validator::make([ + 'name' => $this->_newName, + 'slug' => $slug + ], $rules); + + + if ($validator->fails()) { + return CommandResponse::fail($validator); + } + + $this->_genre->name = $this->_newName; + $this->_genre->slug = $slug; + $this->_genre->save(); + + return CommandResponse::succeed(['message' => 'Genre renamed!']); + } +} diff --git a/app/Genre.php b/app/Genre.php index a09cea1c..dfffb077 100644 --- a/app/Genre.php +++ b/app/Genre.php @@ -20,6 +20,8 @@ namespace Poniverse\Ponyfm; +use DB; +use Illuminate\Database\Eloquent\Relations\Relation; use Poniverse\Ponyfm\Traits\SlugTrait; use Illuminate\Database\Eloquent\Model; @@ -27,7 +29,37 @@ class Genre extends Model { protected $table = 'genres'; protected $fillable = ['name', 'slug']; + protected $appends = ['track_count']; + protected $hidden = ['trackCountRelation']; + public $timestamps = false; use SlugTrait; + + public function tracks(){ + return $this->hasMany(Track::class, 'genre_id'); + } + + /** + * "Dummy" relation to facilitate eager-loading of a genre's track count. + * This relationship should not be used directly. + * + * Inspired by: http://laravel.io/forum/05-03-2014-eloquent-get-count-relation?page=1#reply-6226 + * + * @return Relation + */ + public function trackCountRelation() { + return $this->hasOne(Track::class) + ->select(['genre_id', DB::raw('count(*) as track_count')]) + ->groupBy('genre_id'); + } + + /** + * Returns the number of tracks in this genre. + * + * @return int + */ + public function getTrackCountAttribute() { + return $this->trackCountRelation()->count(); + } } diff --git a/app/Http/Controllers/Api/Web/GenresController.php b/app/Http/Controllers/Api/Web/GenresController.php new file mode 100644 index 00000000..23622b1d --- /dev/null +++ b/app/Http/Controllers/Api/Web/GenresController.php @@ -0,0 +1,51 @@ +. + */ + +namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; + +use Input; +use Poniverse\Ponyfm\Commands\RenameGenreCommand; +use Poniverse\Ponyfm\Genre; +use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; +use Response; + + +class GenresController extends ApiControllerBase +{ + public function getIndex() + { + $this->authorize('access-admin-area'); + + $genres = Genre::with(['trackCountRelation' => function($query) { + $query->withTrashed(); + }])->get(); + + return Response::json([ + 'genres' => $genres->toArray() + ], 200); + } + + + public function putRename($genreId) + { + $command = new RenameGenreCommand($genreId, Input::get('name')); + return $this->execute($command); + } +} diff --git a/app/Http/Controllers/ApiControllerBase.php b/app/Http/Controllers/ApiControllerBase.php index ba2af741..c8420442 100644 --- a/app/Http/Controllers/ApiControllerBase.php +++ b/app/Http/Controllers/ApiControllerBase.php @@ -20,11 +20,12 @@ namespace Poniverse\Ponyfm\Http\Controllers; +use Poniverse\Ponyfm\Commands\CommandBase; use Response; abstract class ApiControllerBase extends Controller { - protected function execute($command) + protected function execute(CommandBase $command) { if (!$command->authorize()) { return $this->notAuthorized(); diff --git a/app/Http/routes.php b/app/Http/routes.php index c8ffaadb..c7e7623e 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -142,6 +142,11 @@ Route::group(['prefix' => 'api/web'], function() { Route::get('/favourites/playlists', 'Api\Web\FavouritesController@getPlaylists'); }); + Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'can:access-admin-area']], function() { + Route::get('/genres', 'Api\Web\GenresController@getIndex'); + Route::put('/genres/{id}', 'Api\Web\GenresController@putRename')->where('id', '\d+'); + }); + Route::post('/auth/logout', 'Api\Web\AuthController@postLogout'); }); diff --git a/app/Policies/GenrePolicy.php b/app/Policies/GenrePolicy.php index 098cc1d1..55fdabe9 100644 --- a/app/Policies/GenrePolicy.php +++ b/app/Policies/GenrePolicy.php @@ -20,15 +20,12 @@ namespace Poniverse\Ponyfm\Policies; +use Poniverse\Ponyfm\Genre; +use Poniverse\Ponyfm\User; + class GenrePolicy { - /** - * Create a new policy instance. - * - * @return void - */ - public function __construct() - { - // stub class + public function rename(User $user, Genre $genre) { + return $user->hasRole('admin'); } } diff --git a/public/templates/admin/genres.html b/public/templates/admin/genres.html index d78ace18..23793e92 100644 --- a/public/templates/admin/genres.html +++ b/public/templates/admin/genres.html @@ -1,3 +1,31 @@

Genre Editor

-

This is a stub page!

+
+ + + + + + + + + + + + + +
Genre# of tracks (including deleted)Actions
+ +
+ {{ genre.errorMessage }} +
+
{{ genre.track_count }} + +
+
diff --git a/resources/assets/scripts/app/app.coffee b/resources/assets/scripts/app/app.coffee index 8ef8afff..20b1891f 100644 --- a/resources/assets/scripts/app/app.coffee +++ b/resources/assets/scripts/app/app.coffee @@ -232,7 +232,8 @@ module.config [ templateUrl: '/templates/admin/_layout.html' state.state 'admin.genres', - url: '/genres', + url: '/genres' + controller: 'admin-genres' templateUrl: '/templates/admin/genres.html' # Homepage diff --git a/resources/assets/scripts/app/controllers/admin-genres.coffee b/resources/assets/scripts/app/controllers/admin-genres.coffee new file mode 100644 index 00000000..62ff027c --- /dev/null +++ b/resources/assets/scripts/app/controllers/admin-genres.coffee @@ -0,0 +1,48 @@ +# Pony.fm - A community for pony fan music. +# Copyright (C) 2015 Peter Deltchev +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +angular.module('ponyfm').controller 'admin-genres', [ + '$scope', '$state', 'admin-genres' + ($scope, $state, genres) -> + + $scope.genres = {} + + setGenres = (genres) -> + for genre in genres + genre.isSaving = false + genre.isError = false + $scope.genres[genre.id] = genre + + genres.fetch().done setGenres + + + # Renames the given genre + $scope.renameGenre = (genre) -> + genre.isSaving = true + genres.rename(genre.id, genre.name) + .done (response)-> + genre.isError = false + .fail (response)-> + genre.errorMessage = response + genre.isError = true + .always (response)-> + genre.isSaving = false + + + # Merges genre1 into genre2 + mergeGenre = (genre1, genre2) -> + # stub method +] diff --git a/resources/assets/scripts/app/directives/on-enter.coffee b/resources/assets/scripts/app/directives/on-enter.coffee new file mode 100644 index 00000000..5e2baaf1 --- /dev/null +++ b/resources/assets/scripts/app/directives/on-enter.coffee @@ -0,0 +1,28 @@ +# Pony.fm - A community for pony fan music. +# Copyright (C) 2015 Peter Deltchev +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +# This directive calls the given function when Enter is pressed in a +# standalone input field. +angular.module('ponyfm').directive 'pfmOnEnter', () -> + (scope, element, attrs) -> + element.bind("keyup", (event) -> + if (event.which is 13) + scope.$apply(()-> + scope.$eval(attrs.pfmOnEnter) + ) + event.preventDefault() + ) diff --git a/resources/assets/scripts/app/services/admin-genres.coffee b/resources/assets/scripts/app/services/admin-genres.coffee new file mode 100644 index 00000000..b36e22c1 --- /dev/null +++ b/resources/assets/scripts/app/services/admin-genres.coffee @@ -0,0 +1,45 @@ +# Pony.fm - A community for pony fan music. +# Copyright (C) 2015 Peter Deltchev +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +angular.module('ponyfm').factory('admin-genres', [ + '$rootScope', '$http' + ($rootScope, $http) -> + def = null + genres = [] + + self = + fetch: () -> + url = '/api/web/admin/genres' + def = new $.Deferred() + $http.get(url).success (genres) -> + def.resolve(genres['genres']) + def.promise() + + rename: (genre_id, new_name) -> + url = "/api/web/admin/genres/#{genre_id}" + def = new $.Deferred() + + $http.put(url, {name: new_name}) + .success (response)-> + def.resolve(response) + + .error (response)-> + def.reject(response) + + def.promise() + + self +]) diff --git a/resources/assets/styles/admin.less b/resources/assets/styles/admin.less new file mode 100644 index 00000000..3a28666d --- /dev/null +++ b/resources/assets/styles/admin.less @@ -0,0 +1,27 @@ +/** + * Pony.fm - A community for pony fan music. + * Copyright (C) 2015 Peter Deltchev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +@import 'base/bootstrap/bootstrap'; +@import 'mixins'; + + +.genre-list { + .-status { + width: 30px; + } +} diff --git a/resources/assets/styles/app.less b/resources/assets/styles/app.less index fddf45ff..73a7deae 100644 --- a/resources/assets/styles/app.less +++ b/resources/assets/styles/app.less @@ -23,6 +23,7 @@ @import 'mixins'; @import 'layout'; @import 'account-content'; +@import 'admin'; @import 'components'; @import 'forms'; @import 'animations'; diff --git a/resources/assets/styles/forms.less b/resources/assets/styles/forms.less index fd78f0b1..c9a7efdd 100644 --- a/resources/assets/styles/forms.less +++ b/resources/assets/styles/forms.less @@ -68,6 +68,20 @@ input[type="text"], input[type="password"], input[type="date"], input[type="numb &:focus { .box-shadow(none); } + + &.x-large { + font-size: 12pt; + padding: 0.5em; + height: 2em; + } + + &.x-saving { + background: @pfm-light-grey; + } + + &.x-error { + background: @btnDangerBackground; + } } select {