Commit ba68e65c authored by Jack Stupple's avatar Jack Stupple

Merge branch 'dev' into 'master'

v1.0.0

See merge request !1
parents 07867317 8b9f4b4e
......@@ -15,3 +15,5 @@ DB_PASSWORD=secret
CACHE_DRIVER=file
QUEUE_DRIVER=sync
QUESTIONS_PER_SEASON=3
\ No newline at end of file
......@@ -3,3 +3,5 @@
Homestead.json
Homestead.yaml
.env
.htpasswd
.DS_Store
RewriteEngine on
RewriteRule . public/index.php [L]
# Managing Money API
> Asgard is built on Laravel which is the 'non-scaled-down' version of Lumen. This means that Vidz probably is best to tinker with the API as she knows how Eloquent works and all that stuff.
## Requirements
```
php >= 7.0
```
The app retrieves most of its dynamic data from this API which is built in [lumen](https://lumen.laravel.com/docs/5.6). Refer to the docs to add additional functionality.
## Session ID
The player is authenticated using a session ID passed in the headers. To get the current player use the helper method `get_player`.
Most of the system works around getting the current user and returns appropriate information for that user. So most controllers get the player and return results related to their character, or language etc. Authentication is handled in the Authentication middleware which just gets the player and if the player returns null then they are not authenticated.
> This is more intended to be an identifier than security. The system does not store user data apart from a user-agent to better understand their device.
>
> There is also basic authentication to protected them from being spammed.
## Migrations
The API uses migrations to manage the database setup. This system allows us to upgrade the server and data within the server easily and reliably. **You shouldn't change an existing migration, instead you should add a new one**, using this command `php artisan make:migration NAME_OF_MIGRATION`.
Once you have made the migration just run `php artisan migrate` to upgrade the database to the latest version.
\ No newline at end of file
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Answer extends Model
{
protected $guarded = [];
protected $casts = [
'language_id' => 'integer'
];
public function questions()
{
return $this->belongsToMany(\App\Question::class);
}
public function modifiers()
{
$player = Player::where('session_id', '=', $_SERVER['HTTP_SESSION_ID'])->first();
if ($player->currency_id) {
return $this->hasMany(\App\Modifier::class, 'model_id')
->where('model', '=', 'answer')
->where('currency_id', '=', $player->currency_id);
} else {
$default_currency = Currency::where('currency_code', 'EUR')->firstOrFail();
return $this->hasMany(\App\Modifier::class, 'model_id')
->where('model', '=', 'answer')
->where('currency_id', '=', $default_currency->id);
}
}
public function resourceCategories()
{
return $this->belongsToMany(\App\ResourceCategory::class);
}
}
\ No newline at end of file
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\Question;
class Character extends Model
{
protected $guarded = [];
protected $casts = [
'enabled' => 'datetime'
];
protected $appends = ['available_questions'];
public function baseModifiers()
{
$player = Player::where('session_id', '=', $_SERVER['HTTP_SESSION_ID'])->first();
if ($player->currency_id) {
return $this->hasMany(Modifier::class, 'model_id')
->where('model', '=', 'character')
->where('currency_id', '=', $player->currency_id);
} else {
$default_currency = Currency::where('currency_code', 'EUR')->firstOrFail();
return $this->hasMany(Modifier::class, 'model_id')
->where('model', '=', 'character')
->where('currency_id', '=', $default_currency->id);
}
}
public function getAvailableQuestionsAttribute()
{
return $this->questions()->count();
}
public function questions()
{
$player = get_player(false);
if ($player->language_id) {
$language = \App\Language::where('id', $player->language_id)->firstOrFail();
} else {
$language = \App\Language::where('slug', 'en')->first();
}
return $this->hasMany(Question::class)
->where('language_id', $language->id);
}
public function paperwork()
{
return $this->hasMany(\App\Paperwork::class);
}
public function status()
{
$player = get_player(false);
return $this->hasOne(\App\CharacterSocial::class)
->where('language_id', $player ? $player->language_id : \App\Language::where('slug', 'en')->first()->id)
->where('season_roundup_position', '=', $player ? $player->season_roundup_position : 0);
}
public function profile_picture()
{
$player = get_player(false);
return $this->hasOne(\App\CharacterPicture::class)
->where('season_roundup_position', '=', $player ? $player->season_roundup_position : 0);
}
}
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\Question;
class CharacterPicture extends Model
{
protected $guarded = [];
protected $casts = [];
public function character()
{
return $this->belongsTo(\App\Character::class);
}
}
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\Question;
class CharacterSocial extends Model
{
protected $table = 'character_social';
protected $guarded = [];
protected $casts = [];
public function character()
{
return $this->belongsTo(\App\Character::class);
}
}
<?php
namespace App\Console\Commands;
use App\Currency;
use App\Language;
use App\Modifier;
use App\Resource;
use App\ResourceCategory;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* Class ImportResources
* @package App\Console\Commands
* @see https://laravel.com/docs/5.6/artisan
*/
class AddCurrency extends Command
{
protected $signature = 'make:currency
{currency_code : Currency code like; GBP or USD}
{--M|multiplier= : Optionally when copying across all modifiers to this new currency, multiply them by this value.}';
protected $description = 'Inject a new currency with a multiplier of the existing values.';
public function handle()
{
if ($this->anticipate('This is intended for debugging, would you like to continue?', ['Yes', 'No'], 'Yes') !== 'Yes') {
$this->comment('No changes made');
return;
}
$currency_code = $this->argument('currency_code');
$multiplier = floatval($this->option('multiplier'));
$this->line('<info>Currency Code:</info> ' . $currency_code);
$this->line('<info>Currency Multiplier:</info> ' . $multiplier);
DB::beginTransaction();
$currency = new Currency;
$currency->currency_code = $currency_code;
$currency->save();
$default_currency = Currency::where('currency_code', 'EUR')->firstOrFail();
$total_modifiers = Modifier::where('currency_id', $default_currency->id)->count();
$this->output->progressStart($total_modifiers);
$modifiers = Modifier::where('currency_id', $default_currency->id)->get();
foreach ($modifiers as $modifier) {
$new_modifier = new Modifier();
$new_modifier->model = $modifier->model;
$new_modifier->model_id = $modifier->model_id;
$new_modifier->modifier_category_id = $modifier->modifier_category_id;
$new_modifier->currency_id = $currency->id;
$new_modifier->amount = $modifier->amount * $multiplier;
$new_modifier->save();
$this->output->progressAdvance();
}
$this->output->progressFinish();
DB::commit();
$this->info('Added new currency and updated modifiers');
}
}
\ No newline at end of file
<?php
namespace App\Console\Commands;
use App\Language;
use App\Resource;
use App\ResourceCategory;
use Illuminate\Console\Command;
/**
* Class ImportResources
* @package App\Console\Commands
* @see https://laravel.com/docs/5.6/artisan
*/
class ImportResources extends Command
{
protected $signature = 'import:resources {csv_file_name}';
protected $description = 'Import resources into the API via a CSV file.';
protected $csv_file_name = null;
protected $csv_file_pathinfo = [];
protected $csv_headers = null;
protected $csv_data = [];
protected static $required_headers = [
'resource groups',
'category',
'title'
];
protected static $cached_langages = [];
protected static $cached_categories = [];
protected static $language_slug_replacement = [
'uk' => 'en'
];
public function handle()
{
ini_set('auto_detect_line_endings', true);
$this->csv_file_name = $this->argument('csv_file_name');
try {
// exception will be thrown if file is invalid
$this->csv_file_pathinfo = $this->checkFileProperties($this->csv_file_name);
} catch (\Exception $exception) {
$this->output->writeln("<error>Error encountered:</error> {$exception->getMessage()}");
return false;
}
list($this->csv_headers, $this->csv_data) = $this->getFileData($this->csv_file_name);
$this->checkHeaders($this->csv_headers); // throws exception if fails
// we now have the resources list
foreach ($this->csv_data as $csv_row_number => $_resource) {
$language = null;
$resource_link = null;
// add here cases for the different kinds of resources we are importing
if (!empty($_resource['uk asset'])) {
$language = $this->getLanguageFor('uk');
$resource_link = $_resource['uk asset'];
} else if (!empty($_resource['fr asset'])) {
$language = $this->getLanguageFor('fr');
$resource_link = $_resource['fr asset'];
} else if (!empty($_resource['de asset'])) {
$language = $this->getLanguageFor('de');
$resource_link = $_resource['de asset'];
} else if (!empty($_resource['no asset'])) {
$language = $this->getLanguageFor('no');
$resource_link = $_resource['no asset'];
}
// check for duplicates
if ($this->isDuplicateResource($language->id, $resource_link)) {
$this->output->writeln("<comment>Skipped row due to duplicate:</comment> {$csv_row_number}");
continue;
}
$resource = new Resource();
$resource->language_id = $language->id;
$resource->name = $_resource['title'];
$resource->link = $resource_link;
if ($resource->save() !== true) {
$this->output->writeln("<error>Failed to save row:</error> {$csv_row_number}");
}
// attach it to the category (this is a belongsToMany relationship. A resource can be in multiple categories, intended for admin)
$resource->categories()->attach($this->getResourceCategory($_resource['category'], $_resource['resource groups']));
}
}
protected function getResourceCategory($category, $parent_category)
{
if (isset(static::$cached_categories[$category])) {
return static::$cached_categories[$category];
}
$resource_category = ResourceCategory::firstOrCreate([
'name' => $category,
'parent_category' => $parent_category
]);
if (!$resource_category->exists()) {
$resource_category->save();
}
static::$cached_categories[$category] = $resource_category;
return $resource_category;
}
protected function isDuplicateResource($language_id, $resource_link)
{
return Resource::where('link', $resource_link)->where('language_id', $language_id)->count();
}
protected function getLanguageFor($language_slug)
{
// convert uk to en just incase it comes up
if (in_array($language_slug, array_keys(static::$language_slug_replacement))) {
$language_slug = static::$language_slug_replacement[$language_slug];
}
if (isset(static::$cached_langages[$language_slug])) {
return static::$cached_langages[$language_slug];
}
$language = Language::where('slug', $language_slug)->firstOrFail();
static::$cached_langages[$language_slug] = $language;
return $language;
}
protected function checkHeaders($csv_headers)
{
foreach (static::$required_headers as $required_header) {
if (in_array($required_header, $csv_headers) === false) {
throw new \Exception ("Missing required header in CSV: {$required_header}");
}
}
}
protected function getFileData($csv_file_name)
{
$file_handle = fopen($csv_file_name, 'r');
$csv_headers = null;
$csv_data = [];
$row_number = 0;
$empty_rows = 0; // if 3 empty rows are encountered we just break the loop- assumed end of data
while (($csv_row = fgetcsv($file_handle)) !== false) {
$row_number++; // the header is also a row in the CSV
if ($csv_headers === null) {
$_csv_headers = [];
foreach ($csv_row as $header) {
$_csv_headers[] = trim(strtolower($header));
}
$csv_headers = $_csv_headers;
unset($_csv_headers);
continue;
}
// skip empty rows
if (array_filter(array_unique($csv_row)) === []) {
$empty_rows++;
if ($empty_rows >= 3) {
// assume we have reached the end of the data
$this->output->writeln("<comment>Assumed end of data:</comment> {$row_number}");
break;
} else {
$this->output->writeln("<comment>Skip empty row:</comment> {$row_number}");
continue;
}
}
$_csv_data = [];
foreach ($csv_headers as $header_index => $header) {
$_csv_data[$header] = trim($csv_row[$header_index]); // we trim to ensure no additional spaces
// ensure the data is expected - primarily for the URL's
$validation_method = camel_case('validate ' . $header);
if (method_exists($this, $validation_method) && !empty($_csv_data[$header])) {
if (!$this->$validation_method($_csv_data[$header])) {
throw new \Exception("Issue with {$header} on row: {$row_number}");
}
}
}
$csv_data[$row_number] = $_csv_data; // add the row to the data with same row number as that in the CSV
unset($_csv_data);
}
return [$csv_headers, $csv_data];
}
protected function checkFileProperties($csv_file_name)
{
if (file_exists($csv_file_name) === false) {
throw new \Exception('File does not exist');
}
$csv_file_pathinfo = pathinfo($csv_file_name);
// extension is only defined if one is provided, eg /etc/hosts will have no extension
if (!isset($csv_file_pathinfo['extension']) || $csv_file_pathinfo['extension'] !== 'csv') {
throw new \Exception('File is not CSV');
}
return $csv_file_pathinfo;
}
protected function validateUkAsset($uk_asset)
{
return $this->validateUrl($uk_asset);
}
protected function validateFrAsset($asset)
{
return $this->validateUrl($asset);
}
protected function validateDeAsset($asset)
{
return $this->validateUrl($asset);
}
protected function validateNoAsset($asset)
{
return $this->validateUrl($asset);
}
protected function validateUrl($asset)
{
return filter_var($asset, FILTER_VALIDATE_URL);
}
}
\ No newline at end of file
......@@ -2,6 +2,8 @@
namespace App\Console;
use App\Console\Commands\AddCurrency;
use App\Console\Commands\ImportResources;
use Illuminate\Console\Scheduling\Schedule;
use Laravel\Lumen\Console\Kernel as ConsoleKernel;
......@@ -13,7 +15,8 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [
//
ImportResources::class,
AddCurrency::class
];
/**
......
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Currency extends Model
{
}
\ No newline at end of file
<?php
namespace App\Http\Controllers\Api;
use App\Answer;
use Illuminate\Http\Request;
class AnswerController extends Controller
{
public function index(Request $request)
{
return Answer::with('modifiers')->get();
}
public function show(Request $request, $answer_id)
{
// ensure that any secret fields are appropriately populated in the model
return Answer::where('id', $answer_id)->with('modifiers')->firstOrFail();
}
}
\ No newline at end of file
<?php
namespace App\Http\Controllers\Api;
use App\Modifier;
use Illuminate\Http\Request;
class AnswerModifierController extends Controller
{
public function index(Request $request, $answer_id)
{
return Modifier::where('model', '=', 'answer')->where('model_id', '=', $answer_id)->get();
}
}
\ No newline at end of file
<?php
namespace App\Http\Controllers\Api;
use App\Character;
use Illuminate\Http\Request;
class CharacterController extends Controller
{
public function index()
{
return Character::with([
'status',
'profile_picture'
])->get();
}
public function show(Request $request, $character_id)
{
return Character::where('id', $character_id)->with([
'baseModifiers',
'baseModifiers.modifierCategory',
'questions',
'questions.options',
'questions.options.modifiers',
'status',
'profile_picture'
])->firstOrFail();
}
}
<?php
namespace App\Http\Controllers\Api;
use App\Character;
use Illuminate\Http\Request;
class CharacterQuestionController extends Controller
{
public function index(Request $request)
{
$player = get_player();
return \App\Character::where('id', $player->character_id)