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!
+
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 {