mirror of
https://github.com/chenasraf/nextcloud-autocurrency.git
synced 2026-05-18 01:29:05 +00:00
feat: poc ready
This commit is contained in:
57
README.md
57
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user