#20: Added the genre creation tool.

This commit is contained in:
Peter Deltchev 2016-01-06 04:50:44 -08:00
parent 1136bdc4e9
commit bf831d839a
11 changed files with 227 additions and 34 deletions

View file

@ -0,0 +1,75 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 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 <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Commands;
use Gate;
use Illuminate\Support\Str;
use Poniverse\Ponyfm\Models\Genre;
use Validator;
class CreateGenreCommand extends CommandBase
{
/** @var Genre */
private $_genreName;
public function __construct($genreName)
{
$this->_genreName = $genreName;
}
/**
* @return bool
*/
public function authorize()
{
return Gate::allows('create-genre');
}
/**
* @throws \Exception
* @return CommandResponse
*/
public function execute()
{
$slug = Str::slug($this->_genreName);
$rules = [
'name' => 'required|unique:genres,name,NULL,id,deleted_at,NULL|max:50',
'slug' => 'required|unique:genres,slug,NULL,id,deleted_at,NULL'
];
$validator = Validator::make([
'name' => $this->_genreName,
'slug' => $slug
], $rules);
if ($validator->fails()) {
return CommandResponse::fail($validator);
}
Genre::create([
'name' => $this->_genreName,
'slug' => $slug
]);
return CommandResponse::succeed(['message' => 'Genre created!']);
}
}

View file

@ -21,6 +21,7 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Input; use Input;
use Poniverse\Ponyfm\Commands\CreateGenreCommand;
use Poniverse\Ponyfm\Commands\DeleteGenreCommand; use Poniverse\Ponyfm\Commands\DeleteGenreCommand;
use Poniverse\Ponyfm\Commands\RenameGenreCommand; use Poniverse\Ponyfm\Commands\RenameGenreCommand;
use Poniverse\Ponyfm\Models\Genre; use Poniverse\Ponyfm\Models\Genre;
@ -45,6 +46,11 @@ class GenresController extends ApiControllerBase
], 200); ], 200);
} }
public function postCreate()
{
$command = new CreateGenreCommand(Input::get('name'));
return $this->execute($command);
}
public function putRename($genreId) public function putRename($genreId)
{ {

View file

@ -153,6 +153,7 @@ Route::group(['prefix' => 'api/web'], function() {
Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'can:access-admin-area']], function() { Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'can:access-admin-area']], function() {
Route::get('/genres', 'Api\Web\GenresController@getIndex'); Route::get('/genres', 'Api\Web\GenresController@getIndex');
Route::post('/genres', 'Api\Web\GenresController@postCreate');
Route::put('/genres/{id}', 'Api\Web\GenresController@putRename')->where('id', '\d+'); Route::put('/genres/{id}', 'Api\Web\GenresController@putRename')->where('id', '\d+');
Route::delete('/genres/{id}', 'Api\Web\GenresController@deleteGenre')->where('id', '\d+'); Route::delete('/genres/{id}', 'Api\Web\GenresController@deleteGenre')->where('id', '\d+');
}); });

View file

@ -49,8 +49,6 @@ class Genre extends Model
protected $appends = ['track_count', 'url']; protected $appends = ['track_count', 'url'];
protected $hidden = ['trackCountRelation']; protected $hidden = ['trackCountRelation'];
public $timestamps = false;
use SlugTrait, SoftDeletes, RevisionableTrait; use SlugTrait, SoftDeletes, RevisionableTrait;
public function tracks(){ public function tracks(){

View file

@ -52,6 +52,10 @@ class AuthServiceProvider extends ServiceProvider
return $user->hasRole('admin'); return $user->hasRole('admin');
}); });
$gate->define('create-genre', function(User $user) {
return $user->hasRole('admin');
});
$this->registerPolicies($gate); $this->registerPolicies($gate);
} }
} }

View file

@ -0,0 +1,49 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 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 <http://www.gnu.org/licenses/>.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddGenreTimestamps extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('genres', function(Blueprint $table) {
$table->nullableTimestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('genres', function(Blueprint $table) {
$table->dropTimestamps();
});
}
}

View file

@ -1,33 +1,55 @@
<h1>Genre Editor</h1> <h1>Genre Editor</h1>
<section class="genre-list stretch-to-bottom"> <div class="stretch-to-bottom">
<table class="table">
<thead> <section class="genre-creator">
<th>Genre</th> <h2>Add genre</h2>
<th class="-status"></th>
<th># of tracks (including deleted)</th> <p>Enter a genre name and press enter to create it!</p>
<th class="-actions">Actions</th> <input
</thead> type="text"
<tr ng-repeat="genre in genres track by genre.id"> class="x-large"
<td> ng-class="{'x-saving': isCreating, 'x-error': hasCreationError}"
<input ng-model="genreToCreate"
type="text" pfm-on-enter="createGenre(genreToCreate)"
class="x-large" />
ng-class="{'x-saving': genre.isSaving, 'x-error': genre.isError}"
ng-model="genre.name" <div class="alert alert-error" ng-show="hasCreationError">
pfm-on-enter="renameGenre(genre)" {{ createGenreError }}
/> </div>
<div class="alert alert-error" ng-show="genre.isError"> </section>
{{ genre.errorMessage }}
</div> <section class="genre-list">
</td> <h2>Rename &amp; delete genres</h2>
<td><i ng-show="genre.isSaving" class="icon-cog icon-spin icon-large"></i></td>
<td><a ng-href="{{ genre.url }}">{{ genre.track_count }}</a></td> <table class="table">
<td class="-actions"> <thead>
<button class="btn btn-warning" ng-hide="mergeInProgress" ng-click="startMerge(genre)">Merge genres in&hellip;</button> <th>Genre</th>
<button class="btn btn-danger" ng-show="mergeInProgress && destinationGenre.id != genre.id" ng-click="finishMerge(genre)">Merge into <em>{{ destinationGenre.name }}</em>&hellip;</button> <th class="-status"></th>
<button class="btn btn-warning" ng-show="mergeInProgress && destinationGenre.id == genre.id" ng-click="cancelMerge()">Cancel merge</button> <th># of tracks (including deleted)</th>
</td> <th class="-actions">Actions</th>
</tr> </thead>
</table> <tr ng-repeat="genre in genres track by genre.id">
</section> <td>
<input
type="text"
class="x-large"
ng-class="{'x-saving': genre.isSaving, 'x-error': genre.isError}"
ng-model="genre.name"
pfm-on-enter="renameGenre(genre)"
/>
<div class="alert alert-error" ng-show="genre.isError">
{{ genre.errorMessage }}
</div>
</td>
<td><i ng-show="genre.isSaving" class="icon-cog icon-spin icon-large"></i></td>
<td><a ng-href="{{ genre.url }}">{{ genre.track_count }}</a></td>
<td class="-actions">
<button class="btn btn-warning" ng-hide="mergeInProgress" ng-click="startMerge(genre)">Merge genres in&hellip;</button>
<button class="btn btn-danger" ng-show="mergeInProgress && destinationGenre.id != genre.id" ng-click="finishMerge(genre)">Merge into <em>{{ destinationGenre.name }}</em>&hellip;</button>
<button class="btn btn-warning" ng-show="mergeInProgress && destinationGenre.id == genre.id" ng-click="cancelMerge()">Cancel merge</button>
</td>
</tr>
</table>
</section>
</div>

View file

@ -20,6 +20,11 @@ angular.module('ponyfm').controller 'admin-genres', [
$scope.genres = [] $scope.genres = []
$scope.isCreating = false
$scope.genreToCreate = ''
$scope.hasCreationError = false
$scope.createGenreError = ''
# Used for merging/deleting genres # Used for merging/deleting genres
$scope.mergeInProgress = false $scope.mergeInProgress = false
$scope.genreToDelete = null $scope.genreToDelete = null
@ -37,6 +42,21 @@ angular.module('ponyfm').controller 'admin-genres', [
loadGenres() loadGenres()
$scope.createGenre = (genreName) ->
$scope.isCreating = true
genres.create(genreName)
.done (response) ->
$scope.hasCreationError = false
$scope.genreToCreate = ''
loadGenres()
.fail (response) ->
$scope.hasCreationError = true
$scope.createGenreError = response
console.log(response)
.always (response) ->
$scope.isCreating = false
# Renames the given genre # Renames the given genre
$scope.renameGenre = (genre) -> $scope.renameGenre = (genre) ->
genre.isSaving = true genre.isSaving = true

View file

@ -28,6 +28,17 @@ angular.module('ponyfm').factory('admin-genres', [
def.resolve(genres['genres']) def.resolve(genres['genres'])
def.promise() def.promise()
create: (name) ->
url = '/api/web/admin/genres'
def = new $.Deferred()
$http.post(url, {name: name})
.success (response) ->
def.resolve(response)
.error (response) ->
def.reject(response)
def.promise()
rename: (genre_id, new_name) -> rename: (genre_id, new_name) ->
url = "/api/web/admin/genres/#{genre_id}" url = "/api/web/admin/genres/#{genre_id}"
def = new $.Deferred() def = new $.Deferred()

View file

@ -19,6 +19,9 @@
@import 'base/bootstrap/bootstrap'; @import 'base/bootstrap/bootstrap';
@import 'mixins'; @import 'mixins';
.genre-creator {
max-width: 400px;
}
.genre-list { .genre-list {
.-status { .-status {

View file

@ -31,7 +31,7 @@ a {
.box-sizing(border-box); .box-sizing(border-box);
padding: 10px; padding: 10px;
h1 { h1, h2 {
margin: 1px 1px 5px; margin: 1px 1px 5px;
font-size: 15pt; font-size: 15pt;
color: #C2889C; color: #C2889C;
@ -39,6 +39,10 @@ a {
overflow: hidden; overflow: hidden;
font-weight: normal; font-weight: normal;
} }
h2 {
font-size: 14pt;
}
} }
.static-page { .static-page {