Compare commits

...

34 commits

Author SHA1 Message Date
Ben c82acc8f52
Small fixes 2021-09-21 20:22:56 +02:00
Ben 01212543fe
Implement Matrix as data backend 2021-09-19 16:55:30 +02:00
Ben 1f14932b28
Fetch ticket list from API 2021-09-18 21:15:03 +02:00
Ben 70d59ebece
Update .gitignore 2021-09-18 21:14:32 +02:00
Ben 0a90ea80b8
Update database tables for matrix data backend
and move API to /api/v1/ticket
2021-09-18 19:28:10 +02:00
Ben 5ccdcec520
Use events to open/close tickets 2021-09-18 14:50:21 +02:00
Ben ec89f04144
Update php-matrix-sdk to guzzle 7 version 2021-09-18 12:20:48 +02:00
Ben 51e9a8270d
Update package.json 2021-09-18 11:03:19 +02:00
Ben 332497ea74
Merge branch 'matrix-backend' into experimental
# Conflicts:
#	lib/Controller/TicketController.php
2021-09-18 10:47:51 +02:00
Ben 811766b242
Merge branch 'ticketliste' into experimental 2021-09-18 10:47:25 +02:00
Ben a7cb94d136
Merge branch 'finns-dungeon' into experimental 2021-09-18 10:40:25 +02:00
Ben 0a7cd19130
Update composer.lock 2021-09-18 10:34:16 +02:00
Ben dd485c8651
Wait for generation of synapse data 2021-09-18 10:33:54 +02:00
Ben 9090d384ef
Add first revision of MatrixService
Does not work, because of a GuzzleHttp version clash
in nextcloud/3rdparty and php-matrix-sdk.
2021-09-15 17:19:48 +02:00
Ben 0113cdad85
podman: Fix and tweak run script 2021-09-15 16:48:52 +02:00
Ben c3064395f8
Use license SPDX notation in composer.json 2021-09-15 16:16:07 +02:00
Ben 6f45a4ee37
podman: Add starting synapse homeserver 2021-09-15 14:30:15 +02:00
Ben f62380a768
Add php-matrix-sdk dependency 2021-09-15 14:29:46 +02:00
Ben 58d8695880
Add first TicketList revision 2021-08-22 18:01:29 +02:00
Finn 29322a4e9c Change ticket entry to contain the data needed 2021-08-22 17:10:23 +02:00
Finn 0e2f6f944d rename note to ticket 2021-08-22 17:03:19 +02:00
Finn 5f45d0a3c8 rename note to ticket 2021-08-22 17:02:03 +02:00
Finn 2252a49467 finish ticket page looks wise 2021-08-22 16:37:16 +02:00
Finn d88bd657ef finish ticket page looks wise 2021-08-22 16:37:12 +02:00
Finn 347181f21a fix docker-run 2021-08-22 16:24:55 +02:00
Finn 3440797555 add header prop and handling of that 2021-08-22 16:23:54 +02:00
Finn abfd450699
Remove crap 2021-08-21 20:09:46 +02:00
Finn 92a41e4865
Add save event listener to save button 2021-08-21 20:07:32 +02:00
Finn 742168215f
Add save button 2021-08-21 20:07:32 +02:00
Finn 8a2d0f9fa3
Add Ticket and a HeaderlessKeyValueTable 2021-08-21 20:07:32 +02:00
Finn 8631a08e5e Remove crap 2021-08-21 20:04:05 +02:00
Finn 2538c745a7 Add save event listener to save button 2021-08-21 19:58:03 +02:00
Finn 3e9484484d Add save button 2021-08-21 19:38:46 +02:00
Finn 81ae89697f Add Ticket and a HeaderlessKeyValueTable 2021-08-21 19:32:36 +02:00
37 changed files with 2313 additions and 802 deletions

209
.gitignore vendored
View file

@ -1,10 +1,207 @@
/.idea/
*.iml
/build/
node_modules/
/.php_cs.cache
/js/
# Created by https://www.toptal.com/developers/gitignore/api/phpstorm+all,phpunit,composer,node,vue
# Edit at https://www.toptal.com/developers/gitignore?templates=phpstorm+all,phpunit,composer,node,vue
### Composer ###
composer.phar
/vendor/
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock
### Node ###
# Logs
logs
*.log
npm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# webpack output
js/
### PhpStorm+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PhpStorm+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
### PHPUnit ###
# Covers PHPUnit
# Reference: https://phpunit.de/
# Generated files
.phpunit.result.cache
.phpunit.cache
# PHPUnit
/app/phpunit.xml
/phpunit.xml
# Build data
/build/
### Vue ###
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore
# TODO: where does this rule come from?
docs/_book
# TODO: where does this rule come from?
test/
# End of https://www.toptal.com/developers/gitignore/api/phpstorm+all,phpunit,composer,node,vue

View file

@ -12,7 +12,7 @@
<category>tools</category>
<bugs>https://gitea.rs485.network/UPschooling/Nextcloud-App/issues</bugs>
<dependencies>
<nextcloud min-version="15" max-version="22"/>
<nextcloud min-version="20" max-version="22"/>
</dependencies>
<navigations>
<navigation>

View file

@ -2,12 +2,15 @@
return [
'resources' => [
'note' => ['url' => '/notes'],
'note_api' => ['url' => '/api/0.1/notes']
'ticket_api' => ['url' => '/api/v1/tickets']
],
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'note_api#preflighted_cors', 'url' => '/api/0.1/{path}',
'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']]
]
[
'name' => 'ticket_api#preflighted_cors',
'url' => '/api/v1/tickets',
'verb' => 'OPTIONS',
'requirements' => ['path' => '.+'],
],
],
];

View file

@ -2,13 +2,22 @@
"name": "upschooling/upschooling",
"description": "UPschooling Support Platform",
"type": "project",
"license": "AGPL",
"license": "AGPL-3.0-or-later",
"authors": [
{
"name": "UPschooling"
}
],
"require": {},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/bziemons/matrix-php-sdk.git"
}
],
"require": {
"aryess/php-matrix-sdk": "dev-feature/guzzle7-update",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^8.5",
"nextcloud/coding-standard": "^0.5.0"
@ -17,7 +26,7 @@
"optimize-autoloader": true,
"classmap-authoritative": true,
"platform": {
"php": "7.2"
"php": "7.2.5"
}
},
"scripts": {
@ -25,4 +34,4 @@
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix"
}
}
}

1105
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ docker rm -f nextcloud
docker run -d --name=nextcloud -p 8080:80 -v "${PWD}:/var/www/html/custom_apps/upschooling" docker.io/nextcloud
docker exec nextcloud chown -R 33 /var/www/html/custom_apps
docker exec nextcloud chmod -R ug+rw /var/www/html/custom_apps
docker exec --user 33 nextcloud bash -c 'cd /var/www/html/custom_apps/upschooling && make'
docker exec --user 33 nextcloud bash -c 'cd /var/www/html/custom_apps/upschooling && make composer'
docker exec --user 33 nextcloud php occ maintenance:install --database "sqlite" --admin-user "admin" --admin-pass "admin"
docker exec --user 33 nextcloud php occ config:system:set --value=true --type=boolean debug
docker exec --user 33 nextcloud php occ app:enable --force upschooling

View file

@ -3,11 +3,25 @@
namespace OCA\UPschooling\AppInfo;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
class Application extends App {
class Application extends App implements IBootstrap {
public const APP_ID = 'upschooling';
public function __construct() {
parent::__construct(self::APP_ID);
}
public function register(IRegistrationContext $context): void
{
// Register the composer autoloader for packages shipped by this app, if applicable
include_once __DIR__ . '/../../vendor/autoload.php';
}
public function boot(IBootContext $context): void
{
// nothing to boot?
}
}

View file

@ -3,12 +3,10 @@
namespace OCA\UPschooling\Controller;
use Closure;
use OCA\UPschooling\Service\NoteNotFound;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCA\UPschooling\Service\NoteNotFound;
trait Errors {
protected function handleNotFound(Closure $callback): DataResponse {
try {

View file

@ -1,80 +0,0 @@
<?php
namespace OCA\UPschooling\Controller;
use OCA\UPschooling\AppInfo\Application;
use OCA\UPschooling\Service\NoteService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
class NoteApiController extends ApiController {
/** @var NoteService */
private $service;
/** @var string */
private $userId;
use Errors;
public function __construct(IRequest $request,
NoteService $service,
$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

@ -3,13 +3,14 @@
namespace OCA\UPschooling\Controller;
use OCA\UPschooling\AppInfo\Application;
use OCA\UPschooling\Service\NoteService;
use OCP\AppFramework\Controller;
use OCA\UPschooling\Service\TicketService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
class NoteController extends Controller {
/** @var NoteService */
class TicketApiController extends ApiController
{
/** @var TicketService */
private $service;
/** @var string */
@ -17,9 +18,8 @@ class NoteController extends Controller {
use Errors;
public function __construct(IRequest $request,
NoteService $service,
$userId) {
public function __construct(IRequest $request, TicketService $service, $userId)
{
parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->userId = $userId;
@ -28,14 +28,16 @@ class NoteController extends Controller {
/**
* @NoAdminRequired
*/
public function index(): DataResponse {
public function index(): DataResponse
{
return new DataResponse($this->service->findAll($this->userId));
}
/**
* @NoAdminRequired
*/
public function show(int $id): DataResponse {
public function show(int $id): DataResponse
{
return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
@ -44,25 +46,23 @@ class NoteController extends Controller {
/**
* @NoAdminRequired
*/
public function create(string $title, string $content): DataResponse {
return new DataResponse($this->service->create($title, $content,
$this->userId));
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 {
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 {
public function destroy(int $id): DataResponse
{
return $this->handleNotFound(function () use ($id) {
return $this->service->delete($id, $this->userId);
});

27
lib/Db/MatrixTicket.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace OCA\UPschooling\Db;
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
class MatrixTicket extends Entity implements JsonSerializable
{
protected $matrixRoom;
protected $matrixAssistedUser; // The ID of the person who created the Ticked. Usually the person who needs help.
protected $matrixHelperUser;
protected $status;
protected $version;
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'matrixRoom' => $this->matrixRoom,
'matrixAssistedUser' => $this->matrixAssistedUser,
'matrixHelperUser' => $this->matrixHelperUser,
'status' => $this->status,
'version' => $this->version,
];
}
}

21
lib/Db/MatrixUser.php Normal file
View file

@ -0,0 +1,21 @@
<?php
namespace OCA\UPschooling\Db;
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
class MatrixUser extends Entity implements JsonSerializable {
protected $userId;
protected $matrixUser;
protected $matrixToken;
public function jsonSerialize(): array {
return [
'id' => $this->id,
'userId' => $this->userId,
'matrixUser' => $this->matrixUser,
'matrixToken' => $this->matrixToken,
];
}
}

View file

@ -1,21 +0,0 @@
<?php
namespace OCA\UPschooling\Db;
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
class Note extends Entity implements JsonSerializable {
protected $title;
protected $content;
protected $userId;
public function jsonSerialize(): array {
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content
];
}
}

View file

@ -8,25 +8,28 @@ use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class NoteMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'upschooling', Note::class);
class TicketMapper extends QBMapper
{
public function __construct(IDBConnection $db)
{
parent::__construct($db, 'upschooling_tickets', MatrixTicket::class);
}
/**
* @param int $id
* @param string $userId
* @return Entity|Note
* @return Entity|MatrixTicket
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException
*/
public function find(int $id, string $userId): Note {
public function find(int $id, MatrixUser $matrixUser): MatrixTicket
{
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('upschooling')
->from('upschooling_tickets')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
->andWhere($qb->expr()->in($matrixUser->getMatrixUser(), ['matrix_assisted_user', 'matrix_helper_user']));
return $this->findEntity($qb);
}
@ -34,12 +37,13 @@ class NoteMapper extends QBMapper {
* @param string $userId
* @return array
*/
public function findAll(string $userId): array {
public function findAll(MatrixUser $matrixUser): array
{
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('upschooling')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
->from('upschooling_tickets')
->where($qb->expr()->in($matrixUser->getMatrixUser(), ['matrix_assisted_user', 'matrix_helper_user']));
return $this->findEntities($qb);
}
}

31
lib/Db/UserMapper.php Normal file
View file

@ -0,0 +1,31 @@
<?php
namespace OCA\UPschooling\Db;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class UserMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'upschooling_users', MatrixUser::class);
}
/**
* @param string $userId
* @return Entity|MatrixUser
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException
* @throws \OCP\DB\Exception
*/
public function find(string $userId): MatrixUser {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('upschooling_users')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
return $this->findEntity($qb);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace OCA\UPschooling\Exceptions;
use OC\OCS\Exception;
use OC\OCS\Result;
class RoomNotFoundException extends Exception {
public function __construct()
{
parent::__construct(new Result(null, 404, 'Room not found'));
}
}

View file

@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\UPschooling\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('upschooling')) {
$table = $schema->createTable('upschooling');
$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'], 'upschooling_user_id_index');
}
return $schema;
}
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace OCA\UPschooling\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version000000Date20210918151800 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('upschooling_tickets')) {
$table = $schema->createTable('upschooling_tickets');
$table->addColumn('id', 'integer', [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('matrix_room', 'string', [
'notnull' => true,
'length' => 200,
]);
$table->addColumn('matrix_assisted_user', 'string', [
'notnull' => true,
'length' => 200,
]);
$table->addColumn('matrix_helper_user', 'string', [
'notnull' => false,
'length' => 200,
]);
// could be optimized by having a "localUser" with foreign key to users.id,
// but that would create consistency issues
$table->addColumn('status', 'string', [
'notnull' => true,
'length' => 63,
]);
$table->addColumn('version', 'integer', [
'notnull' => true,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueConstraint(['matrix_room'], 'upschooling_mx_room_id_uniq');
$table->addIndex(['matrix_room'], 'upschooling_mx_room_id_idx');
}
if (!$schema->hasTable('upschooling_users')) {
$table = $schema->createTable('upschooling_users');
$table->addColumn('id', 'integer', [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('user_id', 'string', [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('matrix_user', 'string', [
'notnull' => true,
'length' => 200,
]);
$table->addColumn('matrix_token', 'string', [
'notnull' => true,
'length' => 200,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueConstraint(['user_id'], 'upschooling_mx_user_nc_uniq');
$table->addUniqueConstraint(['matrix_user'], 'upschooling_mx_user_mx_uniq');
$table->addForeignKeyConstraint('users', ['user_id'], ['uid'], [], 'upschooling_mx_user_nc_fk');
}
return $schema;
}
}

View file

@ -0,0 +1,179 @@
<?php
namespace OCA\UPschooling\Service;
use Aryess\PhpMatrixSdk\Exceptions\MatrixException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixHttpLibException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixRequestException;
use Aryess\PhpMatrixSdk\Exceptions\ValidationException;
use Aryess\PhpMatrixSdk\MatrixClient;
use Aryess\PhpMatrixSdk\Room;
use OCA\UPschooling\Db\MatrixUser;
use OCA\UPschooling\Exceptions\RoomNotFoundException;
use Psr\Log\LoggerInterface;
class MatrixService
{
/** @var LoggerInterface */
private $logger;
/** @var MatrixClient */
private $client;
/** @var string */
private $registrationSecret = "~d9fJpPKDZIV67A7=tPCvok:=fTBLV;MFf=9FRxtAazW@-GwSo";
/**
* @throws MatrixRequestException
* @throws MatrixHttpLibException
* @throws ValidationException
* @throws MatrixException
*/
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
$serverUrl = "http://synapse:8008";
$user = "upschooling";
$this->client = new MatrixClient($serverUrl);
$this->client->login($user, "secret", true);
$this->logger->debug("Logged in as " . $user . " on server " . $serverUrl);
}
/**
* @param string $roomId a room id of an existing and joined room.
* @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property.
* @param array $content the contents as a JSON serializable array.
* @throws RoomNotFoundException
* @throws MatrixException
*/
public function setProperty(string $roomId, string $eventType, array $content)
{
$room = $this->findRoom($roomId);
$room->sendStateEvent($eventType, $content);
$this->logger->debug(
"Set property " . $eventType . " on room " . $roomId,
array("room" => $roomId, "key" => $eventType, "value" => $content)
);
}
/**
* @param string $roomId a room id of an existing and joined room.
* @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property.
* @return array the contents of the room state.
* @throws MatrixException
* @throws RoomNotFoundException
*/
public function getProperty(string $roomId, string $eventType): array
{
$this->findRoom($roomId); // make sure the room exists/is joined
$content = $this->client->api()->getStateEvent($roomId, $eventType);
$this->logger->debug(
"Got property " . $eventType . " from room " . $roomId,
array("room" => $roomId, "key" => $eventType, "value" => $content)
);
return $content;
}
/**
* @param string $roomId a room id of an existing and joined room.
* @throws RoomNotFoundException
* @returns int the origin server timestamp of the most recent event.
*/
public function getLastEventDate(string $roomId): int
{
$room = $this->findRoom($roomId);
$events = $room->getEvents();
if (count($events) === 0) {
$this->logger->debug("Did not have any events for room " . $roomId);
return 0;
} else {
$timestamp = array_get($events[0], 'origin_server_ts', 1);
if ($timestamp === 1) {
$this->logger->debug("Could not find origin_server_ts in last event of room " . $roomId);
} else {
$this->logger->debug("Last event in room " . $roomId . " was at " . $timestamp);
}
return $timestamp;
}
}
/**
* Registers a new matrix user with the local matrix server.
*
* @throws MatrixException
* @throws MatrixHttpLibException
* @throws MatrixRequestException
* @return MatrixUser user object without id or Nextcloud user id set.
*/
public function registerNewUser(): MatrixUser
{
$nonceResponse = $this->client->api()->send(
'GET',
'/register',
null,
[],
[],
'/_synapse/admin/v1',
true
);
$randUsername = trim(str_replace(["/", "+"], ".", base64_encode(random_bytes(6))), "=");
$username = "upschooling_" . $randUsername;
$password = base64_encode(random_bytes(32));
$hmacData = $nonceResponse["nonce"] . "\x00" . $username . "\x00" . $password . "\x00notadmin";
$hmac = hash_hmac("sha1", $hmacData, $this->registrationSecret, false);
$registrationResponse = $this->client->api()->send(
'POST',
'/register',
array(
"nonce" => $nonceResponse["nonce"],
"username" => $username,
"password" => $password,
"displayname" => "UPschooling Support User " . $randUsername,
"admin" => false,
"mac" => $hmac,
),
[],
[],
'/_synapse/admin/v1',
true
);
$matrixUser = new MatrixUser();
$matrixUser->setMatrixUser($registrationResponse["user_id"]);
$matrixUser->setMatrixToken($registrationResponse["access_token"]);
$this->logger->debug("Created a new user: " . $matrixUser->getMatrixUser());
return $matrixUser;
}
/**
* @param string $roomId a room id of an existing and joined room.
* @throws RoomNotFoundException
* @returns Room the room object, if found.
*/
private function findRoom(string $roomId): Room
{
foreach ($this->client->getRooms() as $room) {
if ($room->getRoomId() === $roomId) {
$this->logger->debug("Found room " . $roomId . " on matrix client");
return $room;
}
}
$this->logger->error("Room " . $roomId . " was not found on matrix client");
throw new RoomNotFoundException();
}
/**
* Creates a new room.
*
* @throws MatrixException
* @return string the room id of the newly created room.
*/
public function createRoom(): string
{
$roomId = $this->client->createRoom()->getRoomId();
$this->logger->debug("Created a new room: " . $roomId);
return $roomId;
}
}

View file

@ -1,6 +0,0 @@
<?php
namespace OCA\UPschooling\Service;
class NoteNotFound extends \Exception {
}

View file

@ -1,76 +0,0 @@
<?php
namespace OCA\UPschooling\Service;
use Exception;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCA\UPschooling\Db\Note;
use OCA\UPschooling\Db\NoteMapper;
class NoteService {
/** @var NoteMapper */
private $mapper;
public function __construct(NoteMapper $mapper) {
$this->mapper = $mapper;
}
public function findAll(string $userId): array {
return $this->mapper->findAll($userId);
}
private function handleException(Exception $e): void {
if ($e instanceof DoesNotExistException ||
$e instanceof MultipleObjectsReturnedException) {
throw new NoteNotFound($e->getMessage());
} else {
throw $e;
}
}
public function find($id, $userId) {
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($title, $content, $userId) {
$note = new Note();
$note->setTitle($title);
$note->setContent($content);
$note->setUserId($userId);
return $this->mapper->insert($note);
}
public function update($id, $title, $content, $userId) {
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($id, $userId) {
try {
$note = $this->mapper->find($id, $userId);
$this->mapper->delete($note);
return $note;
} catch (Exception $e) {
$this->handleException($e);
}
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace OCA\UPschooling\Service;
use Aryess\PhpMatrixSdk\Exceptions\MatrixException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixHttpLibException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixRequestException;
use Exception;
use OCA\UPschooling\Db\MatrixTicket;
use OCA\UPschooling\Db\MatrixUser;
use OCA\UPschooling\Db\TicketMapper;
use OCA\UPschooling\Db\UserMapper;
use OCA\UPschooling\Exceptions\RoomNotFoundException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
class TicketService {
/** @var MatrixService */
private $matrix;
/** @var TicketMapper */
private $ticketMapper;
/** @var UserMapper */
private $userMapper;
public function __construct(MatrixService $matrix, TicketMapper $ticketMapper, UserMapper $userMapper) {
$this->matrix = $matrix;
$this->ticketMapper = $ticketMapper;
$this->userMapper = $userMapper;
}
public function findAll(string $userId): array {
$dbTickets = $this->ticketMapper->findAll($this->getOrCreateUser($userId));
return array_map(function ($ticket) { return $this->resolveTicket($ticket); }, $dbTickets);
}
public function find($id, $userId): array {
return $this->resolveTicket($this->ticketMapper->find($id, $this->getOrCreateUser($userId)));
}
public function create($title, $content, $userId): array {
$matrixUser = $this->getOrCreateUser($userId);
// FIXME: room must be joined on the remote support-platform Nextcloud
$roomId = $this->matrix->createRoom();
$this->matrix->setProperty($roomId, "upschooling.ticket", array(
// id is to be set from the remote support-platform Nextcloud
"title" => $title,
"description" => $content,
"status" => "open",
"version" => "1",
));
$ticket = new MatrixTicket();
$ticket->setMatrixRoom($roomId);
$ticket->setMatrixAssistedUser($matrixUser->getMatrixUser());
$ticket->setStatus("open");
$ticket->setVersion(1);
return $this->resolveTicket($this->ticketMapper->insert($ticket));
}
public function update($id, $title, $content, $userId) {
throw new Exception("Not implemented");
}
public function delete($id, $userId) {
throw new Exception("Not implemented");
}
/**
* @param MatrixTicket $ticket the database object.
* @return array a JSON serializable representation of the resolved ticket, for the frontend.
*/
private function resolveTicket(MatrixTicket $ticket): array
{
try {
$matrixTicketContent = $this->matrix->getProperty($ticket->getMatrixRoom(), "upschooling.ticket");
$ticketId = array_get($matrixTicketContent, "id", $ticket->getId());
$title = array_get($matrixTicketContent, "title", "Untitled");
$description = array_get($matrixTicketContent, "description", "");
$lastModified = $this->matrix->getLastEventDate($ticket->getMatrixRoom());
return array(
'ticketId' => $ticketId,
'status' => $ticket->getStatus(),
'lastModified' => $lastModified,
'title' => $title,
'description' => $description,
);
} catch (MatrixException | RoomNotFoundException $e) {
return array(
'ticketId' => $ticket->getId(),
'status' => 'error',
);
}
}
/**
* @param $userId string Nextcloud user id
* @throws MatrixException
* @throws MatrixHttpLibException
* @throws MatrixRequestException
* @throws MultipleObjectsReturnedException
* @throws \OCP\DB\Exception
*/
private function getOrCreateUser(string $userId): MatrixUser
{
try {
return $this->userMapper->find($userId);
} catch (DoesNotExistException $e) {
$matrixUser = $this->matrix->registerNewUser();
$matrixUser->setUserId($userId);
$this->userMapper->insert($matrixUser);
return $matrixUser;
}
}
}

152
package-lock.json generated
View file

@ -7,7 +7,7 @@
"": {
"name": "upschooling",
"version": "19.0.0",
"license": "agpl",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@nextcloud/axios": "^1.6.0",
"@nextcloud/dialogs": "^3.1.2",
@ -2942,17 +2942,6 @@
"node": ">=0.10.0"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -6157,14 +6146,6 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@ -6450,21 +6431,6 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -9046,14 +9012,6 @@
"dev": true,
"peer": true
},
"node_modules/nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -10327,6 +10285,7 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
"deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
"dev": true,
"peer": true,
"engines": {
@ -11549,9 +11508,9 @@
}
},
"node_modules/sockjs-client": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.1.tgz",
"integrity": "sha512-VnVAb663fosipI/m6pqRXakEOw7nvd7TUgdr3PlR/8V2I95QIdwT8L4nMxhyU8SmDBHYXU1TOElaKOmKLfYzeQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.2.tgz",
"integrity": "sha512-ZzRxPBISQE7RpzlH4tKJMQbHM9pabHluk0WBaxAQ+wm/UieeBVBou0p4wVnSQGN9QmpAZygQ0cDIypWuqOFmFQ==",
"dev": true,
"peer": true,
"dependencies": {
@ -11560,7 +11519,7 @@
"faye-websocket": "^0.11.3",
"inherits": "^2.0.4",
"json3": "^3.3.3",
"url-parse": "^1.5.1"
"url-parse": "^1.5.3"
}
},
"node_modules/sockjs-client/node_modules/debug": {
@ -12030,9 +11989,9 @@
}
},
"node_modules/striptags": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/striptags/-/striptags-3.1.1.tgz",
"integrity": "sha1-yMPn/db7S7OjKjt1LltePjgJPr0="
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/striptags/-/striptags-3.2.0.tgz",
"integrity": "sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw=="
},
"node_modules/style-loader": {
"version": "2.0.0",
@ -13091,9 +13050,9 @@
}
},
"node_modules/url-parse": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
"integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
"dev": true,
"peer": true,
"dependencies": {
@ -13810,26 +13769,6 @@
"node": ">=6"
}
},
"node_modules/webpack-dev-server/node_modules/fsevents": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"peer": true,
"dependencies": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
},
"engines": {
"node": ">= 4.0"
}
},
"node_modules/webpack-dev-server/node_modules/glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
@ -16628,17 +16567,6 @@
"integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
"devOptional": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"peer": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -19218,14 +19146,6 @@
"flat-cache": "^3.0.4"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true,
"peer": true
},
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@ -19441,14 +19361,6 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true,
"peer": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -21416,14 +21328,6 @@
"dev": true,
"peer": true
},
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"dev": true,
"optional": true,
"peer": true
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -23405,9 +23309,9 @@
}
},
"sockjs-client": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.1.tgz",
"integrity": "sha512-VnVAb663fosipI/m6pqRXakEOw7nvd7TUgdr3PlR/8V2I95QIdwT8L4nMxhyU8SmDBHYXU1TOElaKOmKLfYzeQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.2.tgz",
"integrity": "sha512-ZzRxPBISQE7RpzlH4tKJMQbHM9pabHluk0WBaxAQ+wm/UieeBVBou0p4wVnSQGN9QmpAZygQ0cDIypWuqOFmFQ==",
"dev": true,
"peer": true,
"requires": {
@ -23416,7 +23320,7 @@
"faye-websocket": "^0.11.3",
"inherits": "^2.0.4",
"json3": "^3.3.3",
"url-parse": "^1.5.1"
"url-parse": "^1.5.3"
},
"dependencies": {
"debug": {
@ -23818,9 +23722,9 @@
"peer": true
},
"striptags": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/striptags/-/striptags-3.1.1.tgz",
"integrity": "sha1-yMPn/db7S7OjKjt1LltePjgJPr0="
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/striptags/-/striptags-3.2.0.tgz",
"integrity": "sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw=="
},
"style-loader": {
"version": "2.0.0",
@ -24631,9 +24535,9 @@
}
},
"url-parse": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
"integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
"dev": true,
"peer": true,
"requires": {
@ -25175,18 +25079,6 @@
"locate-path": "^3.0.0"
}
},
"fsevents": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"optional": true,
"peer": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",

View file

@ -2,19 +2,20 @@
"name": "upschooling",
"description": "A simple Nextcloud app tutorial for building a notes app",
"version": "19.0.0",
"author": "Julius Härtl <jus@bitgrid.net",
"author": "UPschooling",
"contributors": [
"John Molakvoæ <skjnldsv@protonmail.com>"
"Finn",
"Ben"
],
"bugs": {
"url": "https://github.com/nextcloud/app-tutorial/issues"
"url": "https://gitea.rs485.network/UPschooling/Nextcloud-App/issues"
},
"repository": {
"url": "https://github.com/nextcloud/app-tutorial",
"url": "https://gitea.rs485.network/UPschooling/Nextcloud-App.git",
"type": "git"
},
"homepage": "https://github.com/nextcloud/app-tutorial",
"license": "agpl",
"homepage": "https://gitea.rs485.network/UPschooling/Nextcloud-App",
"license": "AGPL-3.0-or-later",
"private": true,
"scripts": {
"build": "NODE_ENV=production webpack --progress --config webpack.js",

View file

@ -5,10 +5,67 @@ IFS=$'\n\t'
DIR="${0%/*}"
podman run -d --name=nextcloud --replace=true -p 8080:80 -v "$DIR:/var/www/html/custom_apps/upschooling" docker.io/nextcloud
# replace containers
podman rm -if synapse
podman rm -if nextcloud
podman run -d --name=nextcloud -p 8080:80 -p 8008:8008 -v "$DIR:/var/www/html/custom_apps/upschooling" docker.io/nextcloud
podman exec nextcloud chown -R 33 /var/www/html/custom_apps
"$DIR/podman-reown.sh"
podman exec --user 33 nextcloud bash -c 'cd /var/www/html/custom_apps/upschooling && make composer'
podman exec --user 33 nextcloud php occ maintenance:install --database sqlite --admin-user admin --admin-pass admin
podman exec --user 33 nextcloud php occ config:system:set --value=true --type=boolean debug
podman exec --user 33 nextcloud php occ app:enable --force upschooling
if podman volume exists synapse-data; then
echo "Found existing synapse-data volume"
else
podman run --rm \
--name=synapse \
--hostname synapse \
"--mount=type=volume,src=synapse-data,dst=/data" \
-e SYNAPSE_SERVER_NAME=synapse \
-e SYNAPSE_REPORT_STATS=no \
docker.io/matrixdotorg/synapse \
generate
echo "Generated fresh synapse-data volume"
fi
podman run -d \
--name=synapse \
"--mount=type=volume,src=synapse-data,dst=/data" \
"--network=container:$(podman inspect --format "{{.Id}}" nextcloud)" \
--hostname synapse \
docker.io/matrixdotorg/synapse
# wait for synapse to start
MAX_TRIES=15
for ((i = 0 ; i < $MAX_TRIES ; i++)); do
if podman logs synapse 2>&1 | grep -q "Synapse now listening on TCP port 8008"; then
echo -e "Synapse has started. \e[1;38;5;2mOK\033[0m"
break
fi
sleep 1
done
if [[ $i -ge $MAX_TRIES ]]; then
echo "Synapse did not start in time! Use \`podman logs synapse\` to investigate"
exit 1
fi
set +e
REGISTER_USER_OUTPUT="$(podman exec synapse register_new_matrix_user -u upschooling -p secret -a -c /data/homeserver.yaml http://localhost:8008)"
REGISTER_USER_SUCCESS=$?
set -e
if [[ "$REGISTER_USER_SUCCESS" != "0" ]]; then
if echo $REGISTER_USER_OUTPUT | grep -q "User ID already taken."; then
echo -e "User @upschooling:synapse already exists. \e[1;38;5;2mOK\033[0m"
else
echo "Could not create user @upschooling:synapse"
echo $REGISTER_USER_OUTPUT
exit 1
fi
else
echo -e "Matrix user @upschooling:synapse created. \e[1;38;5;2mOK\033[0m"
fi

View file

@ -1,214 +1,103 @@
<template>
<div id="content" class="app-upschooling">
<AppNavigation>
<AppNavigationNew v-if="!loading"
:text="t('upschooling', 'New note')"
:disabled="false"
button-id="new-upschooling-button"
button-class="icon-add"
@click="newNote" />
<ul>
<AppNavigationItem v-for="note in notes"
:key="note.id"
:title="note.title ? note.title : t('upschooling', 'New note')"
:class="{active: currentNoteId === note.id}"
@click="openNote(note)">
<template slot="actions">
<ActionButton v-if="note.id === -1"
icon="icon-close"
@click="cancelNewNote(note)">
{{ t('upschooling', 'Cancel note creation') }}
</ActionButton>
<ActionButton v-else
icon="icon-delete"
@click="deleteNote(note)">
{{ t('upschooling', 'Delete note') }}
</ActionButton>
</template>
</AppNavigationItem>
</ul>
</AppNavigation>
<AppNavigation />
<AppContent>
<div v-if="currentNote">
<input ref="title"
v-model="currentNote.title"
type="text"
:disabled="updating">
<textarea ref="content" v-model="currentNote.content" :disabled="updating" />
<input type="button"
class="primary"
:value="t('upschooling', 'Save')"
:disabled="updating || !savePossible"
@click="saveNote">
<div v-if="currentTicket">
<Ticket :ticket="currentTicket" @save-ticket="saveTicket" @show-ticket-list="deselectTicket" />
</div>
<div v-else id="emptycontent">
<div class="icon-file" />
<h2>{{ t('upschooling', 'Create a note to get started') }}</h2>
<div v-else>
<TicketList :tickets="tickets" @open-ticket="openTicket" />
</div>
</AppContent>
</div>
</template>
<script>
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew'
import TicketList from './components/TicketList'
import Ticket from './Ticket'
import '@nextcloud/dialogs/styles/toast.scss'
import { generateUrl } from '@nextcloud/router'
import { showError, showSuccess } from '@nextcloud/dialogs'
import axios from '@nextcloud/axios'
export default {
name: 'App',
components: {
ActionButton,
Ticket,
TicketList,
AppContent,
AppNavigation,
AppNavigationItem,
AppNavigationNew,
},
data() {
return {
notes: [],
currentNoteId: null,
updating: false,
loading: true,
/**
* Return the currently selected ticket object or null, if none is selected.
*
* @type {object|null|undefined}
*/
currentTicket: undefined,
ticketsFetched: false,
tickets: [],
}
},
computed: {
/**
* Return the currently selected note object
* @returns {Object|null}
*/
currentNote() {
if (this.currentNoteId === null) {
return null
}
return this.notes.find((note) => note.id === this.currentNoteId)
},
/**
* Returns true if a note is selected and its title is not empty
* @returns {Boolean}
*/
savePossible() {
return this.currentNote && this.currentNote.title !== ''
},
watch: {
$route: 'fetchTickets',
},
/**
* Fetch list of notes when the component is loaded
*/
async mounted() {
try {
const response = await axios.get(generateUrl('/apps/upschooling/notes'))
this.notes = response.data
} catch (e) {
console.error(e)
showError(t('upschooling', 'Could not fetch notes'))
}
this.loading = false
created() {
this.createExampleContent()
this.fetchTickets()
},
methods: {
/**
* Create a new note and focus the note content field automatically
* @param {Object} note Note object
*/
openNote(note) {
if (this.updating) {
return
}
this.currentNoteId = note.id
this.$nextTick(() => {
this.$refs.content.focus()
})
},
/**
* Action tiggered when clicking the save button
* create a new note or save
*/
saveNote() {
if (this.currentNoteId === -1) {
this.createNote(this.currentNote)
} else {
this.updateNote(this.currentNote)
}
},
/**
* Create a new note and focus the note content field automatically
* The note is not yet saved, therefore an id of -1 is used until it
* has been persisted in the backend
*/
newNote() {
if (this.currentNoteId !== -1) {
this.currentNoteId = -1
this.notes.push({
id: -1,
title: '',
content: '',
})
this.$nextTick(() => {
this.$refs.title.focus()
})
}
},
/**
* Abort creating a new note
*/
cancelNewNote() {
this.notes.splice(this.notes.findIndex((note) => note.id === -1), 1)
this.currentNoteId = null
},
/**
* Create a new note by sending the information to the server
* @param {Object} note Note object
*/
async createNote(note) {
this.updating = true
try {
const response = await axios.post(generateUrl('/apps/upschooling/notes'), note)
const index = this.notes.findIndex((match) => match.id === this.currentNoteId)
this.$set(this.notes, index, response.data)
this.currentNoteId = response.data.id
} catch (e) {
console.error(e)
showError(t('upschooling', 'Could not create the note'))
}
this.updating = false
},
/**
* Update an existing note on the server
* @param {Object} note Note object
*/
async updateNote(note) {
this.updating = true
try {
await axios.put(generateUrl(`/apps/upschooling/notes/${note.id}`), note)
} catch (e) {
console.error(e)
showError(t('upschooling', 'Could not update the note'))
}
this.updating = false
},
/**
* Delete a note, remove it from the frontend and show a hint
* @param {Object} note Note object
*/
async deleteNote(note) {
try {
await axios.delete(generateUrl(`/apps/upschooling/notes/${note.id}`))
this.notes.splice(this.notes.indexOf(note), 1)
if (this.currentNoteId === note.id) {
this.currentNoteId = null
createExampleContent() {
// this obviously shouldn't survive version 1.0
axios.post(
'api/v1/tickets',
{ title: 'Erstes Ticket', content: 'Erstes Beispiel' },
{ headers: { 'Content-Type': 'application/json', Accept: 'application/json' } },
).then((response) => {
if (this.ticketsFetched === false) {
this.tickets.push(response.data)
}
showSuccess(t('upschooling', 'Note deleted'))
} catch (e) {
console.error(e)
showError(t('upschooling', 'Could not delete the note'))
}
}).catch(console.error)
axios.post(
'api/v1/tickets',
{ title: 'Zweites Ticket', content: 'Zweites Beispiel' },
{ headers: { 'Content-Type': 'application/json', Accept: 'application/json' } },
).then((response) => {
if (this.ticketsFetched === false) {
this.tickets.push(response.data)
}
}).catch(console.error)
},
fetchTickets() {
axios.get(
'api/v1/tickets',
{ headers: { Accept: 'application/json' } }
).then((response) => {
if (Array.isArray(response.data)) {
this.ticketsFetched = true
if (response.data.length !== 0) {
this.tickets = response.data
console.debug(this.tickets) // FIXME
} else {
console.debug('Empty ticket list :(')
}
} else {
console.error('API did not return array: ', response)
}
}).catch(console.error)
},
saveTicket(ticketId, data) {
// TODO send to API (dont forget permission check in API)
console.debug('upschooling', 'saveTicket', ticketId, data)
},
openTicket(ticketId) {
this.currentTicket = this.tickets.find((obj) => obj.ticketId === ticketId)
},
deselectTicket() {
this.currentTicket = null
},
},
}

78
src/Ticket.vue Normal file
View file

@ -0,0 +1,78 @@
<template>
<div class="single-ticket">
<div class="header-bar">
<button @click="back">
{{ t('upschooling', 'Ticket Schließen') }}
</button>
<button @click="save">
{{ t('upschooling', 'Speichern') }}
</button>
</div>
<h2>Ticket "ding"</h2>
<KeyValueTable :data-rows="{Id: ticket.ticketId, Name: ticket.title, Status: ticket.status, Geaendert: toLocaleDate(ticket.lastModified)}" />
<br>
<label for="description">{{ t('upschooling', 'Beschreibung') }}</label>
<textarea id="description" v-model.trim.lazy="description" rows="15" />
<br>
<button @click="save">
{{ t('upschooling', 'Speichern') }}
</button>
<hr>
<div class="placeholder" />
</div>
</template>
<script>
import KeyValueTable from './components/KeyValueTable'
export default {
name: 'Ticket',
components: { KeyValueTable },
props: {
ticket: {
type: Object,
default() {
return {}
},
},
},
data() {
return {
description: this.ticket.description,
}
},
methods: {
toLocaleDate(timestamp) {
const date = new Date(timestamp)
return date.toLocaleString()
},
save() {
this.$emit('save-ticket', this.ticket.ticketId, {}) // TODO: give it only the changed data
},
back() {
this.$emit('show-ticket-list')
},
},
}
</script>
<style scoped>
textarea {
width: 100%;
margin: 0;
resize: vertical;
}
.placeholder {
height: 400px;
width: 100%;
background: #f0f0f0;
}
.header-bar {
display: flex;
width: 100%;
flex-direction: row-reverse;
}
</style>

View file

@ -0,0 +1,34 @@
<template>
<table>
<tr v-for="(value, key, i) in dataRows" :key="i">
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
</table>
</template>
<script>
export default {
name: 'HeaderlessKeyValueTable',
props: {
dataRows: {
type: Object,
default: () => ({}),
},
},
}
</script>
<style scoped>
table {
width: 100%;
}
td {
border-bottom: solid #000 1px;
}
tr:nth-child(2n) {
background: #f0f0f0;
}
</style>

View file

@ -0,0 +1,63 @@
<template>
<table>
<thead>
<tr v-for="(value, key, i) in getHeaders" :key="i">
<th>{{ key }}</th>
<th>{{ value }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key, i) in dataRows" :key="i+1">
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
name: 'KeyValueTable',
props: {
dataRows: {
type: Object,
default: () => ({}),
},
header: {
type: Object,
default: () => ({}),
},
},
computed: {
getHeaders() {
const key = Object.keys(this.header)[0]
const value = this.header[key]
if (key && value) return { [key]: value }
return {}
},
},
}
</script>
<style scoped>
table {
width: 100%;
}
td, th {
border-bottom: solid #000 1px;
}
th {
font-weight: bold;
}
thead {
background: #f0f0f0;
}
tbody tr:nth-child(2n) {
background: #f0f0f0;
}
</style>

View file

@ -0,0 +1,103 @@
<template>
<table
id="ticketlist"
class="list-container has-controls">
<thead>
<tr>
<th id="headerName" class="column-name">
<div id="headerName-container">
<a class="name sort columntitle" data-sort="name">
<span>{{ t('upschooling', 'Titel (#Ticket-Nummer)') }}</span>
<span class="sort-indicator icon-triangle-n" />
</a>
</div>
</th>
<th id="headerStatus" class="column-status">
<a class="status sort columntitle" data-sort="status">
<span>{{ t('upschooling', 'Status') }}</span>
<span class="sort-indicator hidden icon-triangle-s" />
</a>
</th>
<th id="headerDate" class="column-mtime">
<a id="modified" class="columntitle" data-sort="mtime">
<span>{{ t('upschooling', 'Zuletzt Geändert') }}</span>
<span class="sort-indicator hidden icon-triangle-s" />
</a>
</th>
</tr>
</thead>
<tbody id="tickettbody">
<tr v-for="item in tickets" :key="item.ticketId">
<td class="filename ui-draggable ui-draggable-handle">
<a class="name" :href="'#ticket-' + item.ticketId" @click="openTicket(item.ticketId)">
<span class="nametext">
<span class="innernametext">{{ item.title }}</span>
<span class="ticket-number"> (<span class="icon icon-ticket" />#{{ item.ticketId }})</span>
</span>
</a>
</td>
<td class="status">
{{ item.status }}
</td>
<td class="date">
<span
class="modified live-relative-timestamp"
:title="toLocaleDate(item.lastModified)"
:data-timestamp="item.lastModified"
style="color:rgb(81,81,81)">
{{ toLocaleDate(item.lastModified) }}
</span>
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
name: 'TicketList',
props: {
tickets: {
type: Array,
default() {
return []
},
},
},
methods: {
toLocaleDate(timestamp) {
const date = new Date(timestamp)
return date.toLocaleString()
},
openTicket(ticketId) {
this.$emit('open-ticket', ticketId)
},
},
}
</script>
<style scoped>
#ticketlist {
margin-top: 4rem;
}
table {
width: 100%;
}
td {
border-bottom: solid #000 1px;
}
tr:nth-child(2n) {
background: #f0f0f0;
}
a .ticket-number {
opacity: 0.5;
}
a:hover .ticket-number, a:focus .ticket-number, a:active .ticket-number {
opacity: 1;
}
</style>

View file

@ -2,16 +2,15 @@
namespace OCA\UPschooling\Tests\Integration\Controller;
use OCA\UPschooling\Controller\TicketApiController;
use OCA\UPschooling\Db\MatrixTicket;
use OCA\UPschooling\Db\TicketMapper;
use OCP\AppFramework\App;
use OCP\IRequest;
use PHPUnit\Framework\TestCase;
use OCA\UPschooling\Db\Note;
use OCA\UPschooling\Db\NoteMapper;
use OCA\UPschooling\Controller\NoteController;
class NoteIntegrationTest extends TestCase {
class MatrixTicketIntegrationTest extends TestCase {
private $controller;
private $mapper;
private $userId = 'john';
@ -30,13 +29,13 @@ class NoteIntegrationTest extends TestCase {
return $this->createMock(IRequest::class);
});
$this->controller = $container->query(NoteController::class);
$this->mapper = $container->query(NoteMapper::class);
$this->controller = $container->query(TicketApiController::class);
$this->mapper = $container->query(TicketMapper::class);
}
public function testUpdate() {
// create a new note that should be updated
$note = new Note();
$note = new MatrixTicket();
$note->setTitle('old_title');
$note->setContent('old_content');
$note->setUserId($this->userId);
@ -44,7 +43,7 @@ class NoteIntegrationTest extends TestCase {
$id = $this->mapper->insert($note)->getId();
// fromRow does not set the fields as updated
$updatedNote = Note::fromRow([
$updatedNote = MatrixTicket::fromRow([
'id' => $id,
'user_id' => $this->userId
]);

View file

@ -0,0 +1,49 @@
<?php
namespace OCA\UPschooling\Tests\Integration\Controller;
use OCA\UPschooling\Db\MatrixUser;
use OCA\UPschooling\Db\UserMapper;
use OCP\AppFramework\App;
use OCP\IRequest;
use PHPUnit\Framework\TestCase;
class MatrixUserIntegrationTest extends TestCase {
private $mapper;
private $userId = 'john';
private $matrixUserId = '@john:synapse';
public function setUp(): void {
$app = new App('upschooling');
$container = $app->getContainer();
// only replace the user id
$container->registerService('userId', function () {
return $this->userId;
});
// we do not care about the request but the controller needs it
$container->registerService(IRequest::class, function () {
return $this->createMock(IRequest::class);
});
$this->mapper = $container->query(UserMapper::class);
}
public function testUpdate() {
// create a new user
$user = new MatrixUser();
$user->setMatrixUser($this->matrixUserId);
$user->setUserId($this->userId);
$this->mapper->insert($user);
// test that user is in database
$result = $this->mapper->find($this->userId);
$this->assertEquals($user, $result->getData());
// clean up
$this->mapper->delete($result->getData());
}
}

View file

@ -2,11 +2,55 @@
namespace OCA\UPschooling\Tests\Unit\Controller;
use OCA\UPschooling\Controller\NoteApiController;
use OCA\UPschooling\Controller\TicketApiController;
use OCA\UPschooling\Service\NoteNotFound;
use OCA\UPschooling\Service\TicketService;
use OCP\AppFramework\Http;
use OCP\IRequest;
use PHPUnit\Framework\TestCase;
class NoteApiControllerTest extends NoteControllerTest {
public function setUp(): void {
parent::setUp();
$this->controller = new NoteApiController($this->request, $this->service, $this->userId);
class NoteApiControllerTest extends TestCase
{
protected $controller;
protected $service;
protected $userId = 'john';
protected $request;
public function setUp(): void
{
$this->request = $this->getMockBuilder(IRequest::class)->getMock();
$this->service = $this->getMockBuilder(TicketService::class)
->disableOriginalConstructor()
->getMock();
$this->controller = new TicketApiController($this->request, $this->service, $this->userId);
}
public function testUpdate()
{
$note = 'just check if this value is returned correctly';
$this->service->expects($this->once())
->method('update')
->with($this->equalTo(3),
$this->equalTo('title'),
$this->equalTo('content'),
$this->equalTo($this->userId))
->will($this->returnValue($note));
$result = $this->controller->update(3, 'title', 'content');
$this->assertEquals($note, $result->getData());
}
public function testUpdateNotFound()
{
// test the correct status code if no note is found
$this->service->expects($this->once())
->method('update')
->will($this->throwException(new NoteNotFound()));
$result = $this->controller->update(3, 'title', 'content');
$this->assertEquals(Http::STATUS_NOT_FOUND, $result->getStatus());
}
}

View file

@ -1,54 +0,0 @@
<?php
namespace OCA\UPschooling\Tests\Unit\Controller;
use PHPUnit\Framework\TestCase;
use OCP\AppFramework\Http;
use OCP\IRequest;
use OCA\UPschooling\Service\NoteNotFound;
use OCA\UPschooling\Service\NoteService;
use OCA\UPschooling\Controller\NoteController;
class NoteControllerTest extends TestCase {
protected $controller;
protected $service;
protected $userId = 'john';
protected $request;
public function setUp(): void {
$this->request = $this->getMockBuilder(IRequest::class)->getMock();
$this->service = $this->getMockBuilder(NoteService::class)
->disableOriginalConstructor()
->getMock();
$this->controller = new NoteController($this->request, $this->service, $this->userId);
}
public function testUpdate() {
$note = 'just check if this value is returned correctly';
$this->service->expects($this->once())
->method('update')
->with($this->equalTo(3),
$this->equalTo('title'),
$this->equalTo('content'),
$this->equalTo($this->userId))
->will($this->returnValue($note));
$result = $this->controller->update(3, 'title', 'content');
$this->assertEquals($note, $result->getData());
}
public function testUpdateNotFound() {
// test the correct status code if no note is found
$this->service->expects($this->once())
->method('update')
->will($this->throwException(new NoteNotFound()));
$result = $this->controller->update(3, 'title', 'content');
$this->assertEquals(Http::STATUS_NOT_FOUND, $result->getStatus());
}
}

View file

@ -2,9 +2,8 @@
namespace OCA\UPschooling\Controller;
use PHPUnit\Framework\TestCase;
use OCP\AppFramework\Http\TemplateResponse;
use PHPUnit\Framework\TestCase;
class PageControllerTest extends TestCase {
private $controller;
@ -14,7 +13,6 @@ class PageControllerTest extends TestCase {
$this->controller = new PageController($request);
}
public function testIndex() {
$result = $this->controller->index();

View file

@ -1,45 +1,61 @@
<?php
namespace OCA\UPschooling\Tests\Unit\Service;
use OCA\UPschooling\Service\NoteNotFound;
use PHPUnit\Framework\TestCase;
namespace Unit\Service;
use OCA\UPschooling\Db\MatrixTicket;
use OCA\UPschooling\Db\TicketMapper;
use OCA\UPschooling\Db\UserMapper;
use OCA\UPschooling\Service\MatrixService;
use OCA\UPschooling\Service\TicketService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCA\UPschooling\Db\Note;
use OCA\UPschooling\Service\NoteService;
use OCA\UPschooling\Db\NoteMapper;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
class NoteServiceTest extends TestCase {
/** @var TicketService */
private $service;
private $mapper;
/** @var MatrixService */
private $matrixService;
/** @var TicketMapper */
private $ticketMapper;
/** @var UserMapper */
private $userMapper;
/** @var string */
private $userId = 'john';
public function setUp(): void {
$this->mapper = $this->getMockBuilder(NoteMapper::class)
$this->ticketMapper = $this->getMockBuilder(TicketMapper::class)
->disableOriginalConstructor()
->getMock();
$this->service = new NoteService($this->mapper);
$this->userMapper = $this->getMockBuilder(UserMapper::class)
->disableOriginalConstructor()
->getMock();
$this->matrixService = new MatrixService(new NullLogger());
$this->service = new TicketService($this->matrixService, $this->ticketMapper, $this->userMapper);
}
public function testUpdate() {
// the existing note
$note = Note::fromRow([
$note = MatrixTicket::fromRow([
'id' => 3,
'title' => 'yo',
'content' => 'nope'
]);
$this->mapper->expects($this->once())
$this->ticketMapper->expects($this->once())
->method('find')
->with($this->equalTo(3))
->will($this->returnValue($note));
// the note when updated
$updatedNote = Note::fromRow(['id' => 3]);
$updatedNote = MatrixTicket::fromRow(['id' => 3]);
$updatedNote->setTitle('title');
$updatedNote->setContent('content');
$this->mapper->expects($this->once())
$this->ticketMapper->expects($this->once())
->method('update')
->with($this->equalTo($updatedNote))
->will($this->returnValue($updatedNote));
@ -50,9 +66,9 @@ class NoteServiceTest extends TestCase {
}
public function testUpdateNotFound() {
$this->expectException(NoteNotFound::class);
// $this->expectException(NoteNotFound::class);
// test the correct status code if no note is found
$this->mapper->expects($this->once())
$this->ticketMapper->expects($this->once())
->method('find')
->with($this->equalTo(3))
->will($this->throwException(new DoesNotExistException('')));

View file

@ -1,3 +1,3 @@
<?php
require_once __DIR__ . '/../../../tests/bootstrap.php';
require_once __DIR__ . '/../tests/bootstrap.php';