#20: Implemented the genre merging tool.

This commit is contained in:
Peter Deltchev 2015-12-05 18:41:12 -08:00
parent 3ba8467870
commit 07bb5e2c3a
18 changed files with 404 additions and 15 deletions

View file

@ -0,0 +1,76 @@
<?php
/**
* 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 <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Commands;
use Gate;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Poniverse\Ponyfm\Genre;
use Poniverse\Ponyfm\Jobs\DeleteGenre;
use Validator;
class DeleteGenreCommand extends CommandBase
{
use DispatchesJobs;
/** @var Genre */
private $_genreToDelete;
private $_destinationGenre;
public function __construct($genreId, $destinationGenreId) {
$this->_genreToDelete = Genre::find($genreId);
$this->_destinationGenre = Genre::find($destinationGenreId);
}
/**
* @return bool
*/
public function authorize() {
return Gate::allows('delete', $this->_genreToDelete);
}
/**
* @throws \Exception
* @return CommandResponse
*/
public function execute() {
$rules = [
'genre_to_delete' => 'required',
'destination_genre' => 'required',
];
// The validation will fail if the genres don't exist
// because they'll be null.
$validator = Validator::make([
'genre_to_delete' => $this->_genreToDelete,
'destination_genre' => $this->_destinationGenre,
], $rules);
if ($validator->fails()) {
return CommandResponse::fail($validator);
}
$this->dispatch(new DeleteGenre($this->_genreToDelete, $this->_destinationGenre));
return CommandResponse::succeed(['message' => 'Genre deleted!']);
}
}

View file

@ -33,7 +33,7 @@ class RenameGenreCommand extends CommandBase
public function __construct($genreId, $newName)
{
$this->_genre = Genre::find($genreId);;
$this->_genre = Genre::find($genreId);
$this->_newName = $newName;
}

View file

@ -22,6 +22,7 @@ namespace Poniverse\Ponyfm;
use DB;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\SoftDeletes;
use Poniverse\Ponyfm\Traits\SlugTrait;
use Illuminate\Database\Eloquent\Model;
use URL;
@ -30,13 +31,14 @@ use Venturecraft\Revisionable\RevisionableTrait;
class Genre extends Model
{
protected $table = 'genres';
protected $fillable = ['name', 'slug'];
protected $appends = ['track_count', 'url'];
protected $hidden = ['trackCountRelation'];
public $timestamps = false;
use SlugTrait, RevisionableTrait;
use SlugTrait, SoftDeletes, RevisionableTrait;
public function tracks(){
return $this->hasMany(Track::class, 'genre_id');

View file

@ -21,6 +21,7 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Input;
use Poniverse\Ponyfm\Commands\DeleteGenreCommand;
use Poniverse\Ponyfm\Commands\RenameGenreCommand;
use Poniverse\Ponyfm\Genre;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
@ -50,4 +51,11 @@ class GenresController extends ApiControllerBase
$command = new RenameGenreCommand($genreId, Input::get('name'));
return $this->execute($command);
}
public function deleteGenre($genreId)
{
$command = new DeleteGenreCommand($genreId, Input::get('destination_genre_id'));
return $this->execute($command);
}
}

View file

@ -145,6 +145,7 @@ Route::group(['prefix' => 'api/web'], function() {
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::delete('/genres/{id}', 'Api\Web\GenresController@deleteGenre')->where('id', '\d+');
});
Route::post('/auth/logout', 'Api\Web\AuthController@postLogout');

78
app/Jobs/DeleteGenre.php Normal file
View file

@ -0,0 +1,78 @@
<?php
/**
* 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 <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Jobs;
use Auth;
use Poniverse\Ponyfm\Genre;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Bus\SelfHandling;
use Illuminate\Contracts\Queue\ShouldQueue;
use Poniverse\Ponyfm\Track;
use SerializesModels;
class DeleteGenre extends Job implements SelfHandling, ShouldQueue
{
use InteractsWithQueue, SerializesModels;
protected $executingUser;
protected $genreToDelete;
protected $destinationGenre;
/**
* Create a new job instance.
*
* @param Genre $genreToDelete
* @param Genre $destinationGenre
*/
public function __construct(Genre $genreToDelete, Genre $destinationGenre)
{
$this->executingUser = Auth::user();
$this->genreToDelete = $genreToDelete;
$this->destinationGenre = $destinationGenre;
// The genre is deleted synchronously before the job is executed in
// order to prevent race conditions.
$this->genreToDelete->delete();
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
// The user who kicked off this job is used when generating revision log entries.
Auth::login($this->executingUser);
// This is done instead of a single UPDATE query in order to
// generate revision logs for the change.
$this->genreToDelete->tracks()->chunk(200, function ($tracks) {
foreach ($tracks as $track) {
/** @var Track $track */
$track->genre_id = $this->destinationGenre->id;
$track->save();
$track->updateTags();
}
});
}
}

View file

@ -0,0 +1,48 @@
<?php
/**
* 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 <http://www.gnu.org/licenses/>.
*/
use Illuminate\Contracts\Database\ModelIdentifier;
/**
* Class SerializesModels
* This version of the SerializesModel trait overrides a method to make it work
* with soft-deletable models.
*
* @link https://github.com/laravel/framework/issues/9347#issuecomment-120803564
*/
trait SerializesModels {
use \Illuminate\Queue\SerializesModels;
/**
* Get the restored property value after deserialization.
*
* @param mixed $value
* @return mixed
*/
protected function getRestoredPropertyValue($value) {
if ($value instanceof ModelIdentifier) {
return method_exists($value->class, 'withTrashed')
? (new $value->class)->withTrashed()->findOrFail($value->id)
: (new $value->class)->findOrFail($value->id);
} else {
return $value;
}
}
}

View file

@ -28,4 +28,8 @@ class GenrePolicy
public function rename(User $user, Genre $genre) {
return $user->hasRole('admin');
}
public function delete(User $user, Genre $genre) {
return $user->hasRole('admin');
}
}

View file

@ -24,7 +24,6 @@ use Auth;
use Cache;
use Config;
use DB;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Poniverse\Ponyfm\Traits\SlugTrait;
use Exception;
use External;

View file

@ -13,7 +13,8 @@
"barryvdh/laravel-ide-helper": "^2.1",
"guzzlehttp/guzzle": "~6.0",
"doctrine/dbal": "^2.5",
"venturecraft/revisionable": "^1.23"
"venturecraft/revisionable": "^1.23",
"pda/pheanstalk": "~3.0"
},
"require-dev": {
"fzaninotto/faker": "~1.4",

54
composer.lock generated
View file

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "5f66a059010df46b5b6e50c3e4056e17",
"content-hash": "07e7a5fff5a8914a7ced3d14959d194f",
"hash": "edca1732ab37f49b64614d5729d652d3",
"content-hash": "b476009ee841e5b048e73b4fab8372ee",
"packages": [
{
"name": "barryvdh/laravel-ide-helper",
@ -1622,6 +1622,56 @@
],
"time": "2015-07-14 17:31:05"
},
{
"name": "pda/pheanstalk",
"version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/pda/pheanstalk.git",
"reference": "430e77c551479aad0c6ada0450ee844cf656a18b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pda/pheanstalk/zipball/430e77c551479aad0c6ada0450ee844cf656a18b",
"reference": "430e77c551479aad0c6ada0450ee844cf656a18b",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Pheanstalk\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Annesley",
"email": "paul@annesley.cc",
"homepage": "http://paul.annesley.cc/",
"role": "Developer"
}
],
"description": "PHP client for beanstalkd queue",
"homepage": "https://github.com/pda/pheanstalk",
"keywords": [
"beanstalkd"
],
"time": "2015-08-07 21:42:41"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "2.0.4",

View file

@ -0,0 +1,49 @@
<?php
/**
* 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 <http://www.gnu.org/licenses/>.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddDeletedAtColumnToGenres extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('genres', function(Blueprint $table) {
$table->softDeletes()->index();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('genres', function(Blueprint $table) {
$table->dropSoftDeletes();
});
}
}

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateFailedJobsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('failed_jobs', function (Blueprint $table) {
$table->increments('id');
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->timestamp('failed_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('failed_jobs');
}
}

View file

@ -6,7 +6,7 @@
<th>Genre</th>
<th class="-status"></th>
<th># of tracks (including deleted)</th>
<th>Actions</th>
<th class="-actions">Actions</th>
</thead>
<tr ng-repeat="genre in genres">
<td>
@ -23,8 +23,10 @@
</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>
<button class="btn btn-warning" disabled>Merge&hellip;</button>
<td class="-actions">
<button class="btn btn-warning" ng-hide="mergeInProgress" ng-click="startMerge(genre)">Merge&hellip;</button>
<button class="btn btn-danger" ng-show="mergeInProgress && genreToDelete.id != genre.id" ng-click="finishMerge(genre)">Merge in <em>{{ genreToDelete.name }}</em>&hellip;</button>
<button class="btn btn-warning" ng-show="mergeInProgress && genreToDelete.id == genre.id" ng-click="cancelMerge()">Cancel merge</button>
</td>
</tr>
</table>

View file

@ -20,14 +20,22 @@ angular.module('ponyfm').controller 'admin-genres', [
$scope.genres = []
# Used for merging/deleting genres
$scope.mergeInProgress = false
$scope.genreToDelete = null
setGenres = (genres) ->
$scope.genres = []
for genre in genres
genre.isSaving = false
genre.isError = false
$scope.genres.push(genre)
loadGenres = () ->
genres.fetch().done setGenres
loadGenres()
# Renames the given genre
$scope.renameGenre = (genre) ->
@ -42,7 +50,17 @@ angular.module('ponyfm').controller 'admin-genres', [
genre.isSaving = false
# Merges genre1 into genre2
mergeGenre = (genre1, genre2) ->
# stub method
$scope.startMerge = (genreToDelete) ->
$scope.genreToDelete = genreToDelete
$scope.mergeInProgress = true
$scope.cancelMerge = () ->
$scope.genreToDelete = null
$scope.mergeInProgress = false
$scope.finishMerge = (destinationGenre) ->
$scope.mergeInProgress = false
genres.merge($scope.genreToDelete.id, destinationGenre.id)
.done (response) ->
loadGenres()
]

View file

@ -41,5 +41,17 @@ angular.module('ponyfm').factory('admin-genres', [
def.promise()
merge: (genre_id_to_delete, destination_genre_id) ->
url = "/api/web/admin/genres/#{genre_id_to_delete}"
def = new $.Deferred()
$http.delete(url, {params: {destination_genre_id: destination_genre_id}})
.success (response)->
def.resolve(response)
.error (response)->
def.reject(response)
def.promise()
self
])

View file

@ -24,4 +24,8 @@
.-status {
width: 30px;
}
.-actions {
width: 300px;
}
}

View file

@ -36,5 +36,9 @@ cp -n "/vagrant/resources/environments/.env.local" "/vagrant/.env"
php artisan migrate
php artisan db:seed
echo "Now - if you haven't already, SSH into the VM and run \`php artisan poni:setup\`!"
echo "See the README for more details."
echo ""
echo "+-----------------------------------------------+"
echo "| Now - if you haven't already, SSH into the VM |"
echo "| and run \`php artisan poni:setup\`! |"
echo "| See the README for more details. |"
echo "+-----------------------------------------------+"