feat: poc ready

This commit is contained in:
Chen Asraf
2023-02-02 01:37:31 +02:00
parent c32cdaf38d
commit e54cb41c5b
9 changed files with 58 additions and 370 deletions

View File

@@ -4,24 +4,53 @@ SPDX-License-Identifier: CC0-1.0
-->
# Auto Currency
Place this app in **nextcloud/apps/**
## Building the app
This NextCloud app automatically fetches currency information for your Cospend projects, and fills
them up using the main currency as base. No more manually updating exchange rates!
It will automatically run once a day and use your currency names to fetch the correct rate.
The name will be fetched using the first 3-uppercase-letter appearance in the name.
For example:
- USD
- $ USD
- USD $
- United States Dollars (USD)
Will all be considered "USD" for conversion purposes.
This rule applies to main and additional currencies.
## Installation
Place this app in **nextcloud/apps/** or **nextcloud/custom_apps/**
## Development
### Building the app
The app can be built by using the provided Makefile by running:
make
This requires the following things to be present:
* make
* which
* tar: for building the archive
* curl: used if phpunit and composer are not installed to fetch them from the web
* npm: for building and testing everything JS, only required if a package.json is placed inside the **js/** folder
The make command will install or update Composer dependencies if a composer.json is present and also **npm run build** if a package.json is present in the **js/** folder. The npm **build** script should use local paths for build systems and package managers, so people that simply want to build the app won't need to install npm libraries globally, e.g.:
- make
- which
- tar: for building the archive
- curl: used if phpunit and composer are not installed to fetch them from the web
- npm: for building and testing everything JS, only required if a package.json is placed inside the
**js/** folder
The make command will install or update Composer dependencies if a composer.json is present and also
**npm run build** if a package.json is present in the **js/** folder. The npm **build** script
should use local paths for build systems and package managers, so people that simply want to build
the app won't need to install npm libraries globally, e.g.:
**package.json**:
```json
"scripts": {
"test": "node node_modules/gulp-cli/bin/gulp.js karma",
@@ -30,8 +59,7 @@ The make command will install or update Composer dependencies if a composer.json
}
```
## Publish to App Store
### Publish to App Store
First get an account for the [App Store](http://apps.nextcloud.com/) then run:
@@ -39,14 +67,17 @@ First get an account for the [App Store](http://apps.nextcloud.com/) then run:
The archive is located in build/artifacts/appstore and can then be uploaded to the App Store.
## Running tests
### Running tests
You can use the provided Makefile to run all tests by using:
make test
This will run the PHP unit and integration tests and if a package.json is present in the **js/** folder will execute **npm run test**
This will run the PHP unit and integration tests and if a package.json is present in the **js/**
folder will execute **npm run test**
Of course you can also install [PHPUnit](http://phpunit.de/getting-started.html) and use the configurations directly:
Of course you can also install [PHPUnit](http://phpunit.de/getting-started.html) and use the
configurations directly:
phpunit -c phpunit.xml

View File

@@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\AutoCurrency\Controller;
use Closure;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCA\AutoCurrency\Service\NoteNotFound;
trait Errors {
protected function handleNotFound(Closure $callback): DataResponse {
try {
return new DataResponse($callback());
} catch (NoteNotFound $e) {
$message = ['message' => $e->getMessage()];
return new DataResponse($message, Http::STATUS_NOT_FOUND);
}
}
}

View File

@@ -1,80 +0,0 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\AutoCurrency\Controller;
use OCA\AutoCurrency\AppInfo\Application;
use OCA\AutoCurrency\Service\NoteService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
class NoteApiController extends ApiController {
private NoteService $service;
private ?string $userId;
use Errors;
public function __construct(IRequest $request,
NoteService $service,
?string $userId) {
parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->userId = $userId;
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*/
public function index(): DataResponse {
return new DataResponse($this->service->findAll($this->userId));
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*/
public function show(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*/
public function create(string $title, string $content): DataResponse {
return new DataResponse($this->service->create($title, $content,
$this->userId));
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*/
public function update(int $id, string $title,
string $content): DataResponse {
return $this->handleNotFound(function () use ($id, $title, $content) {
return $this->service->update($id, $title, $content, $this->userId);
});
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*/
public function destroy(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) {
return $this->service->delete($id, $this->userId);
});
}
}

View File

@@ -1,70 +0,0 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\AutoCurrency\Controller;
use OCA\AutoCurrency\AppInfo\Application;
use OCA\AutoCurrency\Service\NoteService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
class NoteController extends Controller {
private NoteService $service;
private ?string $userId;
use Errors;
public function __construct(IRequest $request,
NoteService $service,
?string $userId) {
parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->userId = $userId;
}
/**
* @NoAdminRequired
*/
public function index(): DataResponse {
return new DataResponse($this->service->findAll($this->userId));
}
/**
* @NoAdminRequired
*/
public function show(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
}
/**
* @NoAdminRequired
*/
public function create(string $title, string $content): DataResponse {
return new DataResponse($this->service->create($title, $content,
$this->userId));
}
/**
* @NoAdminRequired
*/
public function update(int $id, string $title,
string $content): DataResponse {
return $this->handleNotFound(function () use ($id, $title, $content) {
return $this->service->update($id, $title, $content, $this->userId);
});
}
/**
* @NoAdminRequired
*/
public function destroy(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) {
return $this->service->delete($id, $this->userId);
});
}
}

View File

@@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\AutoCurrency\Controller;
use OCA\AutoCurrency\AppInfo\Application;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest;
use OCP\BackgroundJob\IJobList;
use OCP\Util;
class PageController extends Controller {
private IJobList $jobList;
public function __construct(IRequest $request, IJobList $jobList) {
parent::__construct(Application::APP_ID, $request);
$this->jobList = $jobList;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function index(): TemplateResponse {
Util::addScript(Application::APP_ID, 'autocurrency-main');
// $this->jobList->add('OCA\AutoCurrency\BackgroundJob\FetchCurrenciesJob');
return new TemplateResponse(Application::APP_ID, 'main');
}
}

View File

@@ -16,8 +16,8 @@ class FetchCurrenciesJob extends TimedJob {
$this->logger = $logger;
// Run once a day
$this->setInterval(60);
// $this->setTimeSensitivity(\OCP\BackgroundJob\IJob::TIME_INSENSITIVE);
$this->setInterval(3600 * 24);
$this->setTimeSensitivity(\OCP\BackgroundJob\IJob::TIME_INSENSITIVE);
}
protected function run($arguments) {

View File

@@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\AutoCurrency\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;
class Version000000Date20181013124731 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('autocurrency')) {
$table = $schema->createTable('autocurrency');
$table->addColumn('id', 'integer', [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('title', 'string', [
'notnull' => true,
'length' => 200
]);
$table->addColumn('user_id', 'string', [
'notnull' => true,
'length' => 200,
]);
$table->addColumn('content', 'text', [
'notnull' => true,
'default' => ''
]);
$table->setPrimaryKey(['id']);
$table->addIndex(['user_id'], 'autocurrency_user_id_index');
}
return $schema;
}
}

View File

@@ -33,35 +33,32 @@ class FetchCurrenciesService {
$currencyMap = [];
foreach ($projects as $project) {
$baseCurrency = $this->getCurrencyName($project->getCurrencyname());
$base = $this->getCurrencyName($project->getCurrencyname());
if (isset($currencyMap[$baseCurrency])) {
$json = $currencyMap[$baseCurrency];
if (isset($currencyMap[$base])) {
$json = $currencyMap[$base];
} else {
// request currency exchange rates from the API
// $this->logger->info('Fetching exchange rates for base currency ' . $baseCurrency);
print('Fetching exchange rates for base currency ' . $baseCurrency);
$fp = fopen(str_replace('{base}', $baseCurrency, FetchCurrenciesService::$EXCHANGE_URL), 'r');
$this->logger->info('Fetching exchange rates for base currency ' . $base);
$fp = fopen(str_replace('{base}', $base, FetchCurrenciesService::$EXCHANGE_URL), 'r');
$data = stream_get_contents($fp);
fclose($fp);
$json = json_decode($data, true);
// $this->logger->info('Fetched exchange rates for base currency: ' . json_encode($json));
print('Fetched exchange rates for base currency: ' . json_encode($json));
$this->logger->info('Fetched exchange rates for base currency: ' . json_encode($json));
if ($json['success'] == false) {
// $this->logger->error(new \Error('Failed to fetch exchange rates for base currency ' . $baseCurrency));
print(new \Error('Failed to fetch exchange rates for base currency ' . $baseCurrency));
$this->logger->error(new \Error('Failed to fetch exchange rates for base currency ' . $base));
continue;
}
$currencyMap[$baseCurrency] = $json;
$currencyMap[$base] = $json;
}
$currencies = $this->findAll($project->id);
foreach ($currencies as $currency) {
$cur = $this->getCurrencyName($currency->getName());
$currency->setExchangeRate(1 / $json['rates'][$cur]);
// $this->logger->info('Setting exchange rate for currency ' . $cur . ' to ' . $json['rates'][$cur]);
print('Setting exchange rate for currency ' . $cur . ' to ' . (1 / $json['rates'][$cur]));
$newRate = floatval(number_format(1 / $json['rates'][$cur], 2));
$currency->setExchangeRate($newRate);
$this->logger->info('Setting exchange rate for currency ' . $cur . ' to ' . $newRate);
$this->currencyMapper->update($currency);
}
}
@@ -71,8 +68,7 @@ class FetchCurrenciesService {
// find 3-letter currency code for the base currency
preg_match('/([A-Z]{3})/', $name, $matches);
// $this->logger->info('Matches: ' . json_encode($matches));
print('Matches: ' . json_encode($matches));
$this->logger->info('Matches: ' . json_encode($matches));
if (count($matches) === 2) {
$name = $matches[1];
@@ -94,7 +90,6 @@ class FetchCurrenciesService {
private function handleException(Exception $e) {
if ($e instanceof DoesNotExistException ||
$e instanceof MultipleObjectsReturnedException) {
// TODO determine type
throw new CurrencyNotFound($e->getMessage());
} else {
throw $e;

View File

@@ -1,83 +0,0 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\AutoCurrency\Service;
use Exception;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCA\AutoCurrency\Db\Note;
use OCA\AutoCurrency\Db\NoteMapper;
class NoteService {
private NoteMapper $mapper;
public function __construct(NoteMapper $mapper) {
$this->mapper = $mapper;
}
/**
* @return list<Note>
*/
public function findAll(string $userId): array {
return $this->mapper->findAll($userId);
}
/**
* @return never
*/
private function handleException(Exception $e) {
if ($e instanceof DoesNotExistException ||
$e instanceof MultipleObjectsReturnedException) {
throw new NoteNotFound($e->getMessage());
} else {
throw $e;
}
}
public function find(int $id, string $userId): Note {
try {
return $this->mapper->find($id, $userId);
// in order to be able to plug in different storage backends like files
// for instance it is a good idea to turn storage related exceptions
// into service related exceptions so controllers and service users
// have to deal with only one type of exception
} catch (Exception $e) {
$this->handleException($e);
}
}
public function create(string $title, string $content, string $userId): Note {
$note = new Note();
$note->setTitle($title);
$note->setContent($content);
$note->setUserId($userId);
return $this->mapper->insert($note);
}
public function update(int $id, string $title, string $content, string $userId): Note {
try {
$note = $this->mapper->find($id, $userId);
$note->setTitle($title);
$note->setContent($content);
return $this->mapper->update($note);
} catch (Exception $e) {
$this->handleException($e);
}
}
public function delete(int $id, string $userId): Note {
try {
$note = $this->mapper->find($id, $userId);
$this->mapper->delete($note);
return $note;
} catch (Exception $e) {
$this->handleException($e);
}
}
}