Implement Matrix as data backend

This commit is contained in:
Ben 2021-09-19 16:55:30 +02:00
parent 1f14932b28
commit 01212543fe
Signed by: ben
GPG Key ID: 0F54A7ED232D3319
14 changed files with 338 additions and 156 deletions

8
composer.lock generated
View File

@ -12,12 +12,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/bziemons/matrix-php-sdk.git", "url": "https://github.com/bziemons/matrix-php-sdk.git",
"reference": "54aa76a82237b1abea6b6d7157b03b39d207846c" "reference": "f0d759e56bf7013c1b0d9936acd1406ebcfcecb9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/bziemons/matrix-php-sdk/zipball/54aa76a82237b1abea6b6d7157b03b39d207846c", "url": "https://api.github.com/repos/bziemons/matrix-php-sdk/zipball/f0d759e56bf7013c1b0d9936acd1406ebcfcecb9",
"reference": "54aa76a82237b1abea6b6d7157b03b39d207846c", "reference": "f0d759e56bf7013c1b0d9936acd1406ebcfcecb9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -73,7 +73,7 @@
"support": { "support": {
"source": "https://github.com/bziemons/matrix-php-sdk/tree/feature/guzzle7-update" "source": "https://github.com/bziemons/matrix-php-sdk/tree/feature/guzzle7-update"
}, },
"time": "2021-09-18T10:11:20+00:00" "time": "2021-09-19T12:54:20+00:00"
}, },
{ {
"name": "guzzlehttp/guzzle", "name": "guzzlehttp/guzzle",

View File

@ -45,8 +45,7 @@ class TicketApiController extends ApiController
public function create(string $title, string $content): DataResponse public function create(string $title, string $content): DataResponse
{ {
return new DataResponse($this->service->create($title, $content, return new DataResponse($this->service->create($title, $content, $this->userId));
$this->userId));
} }
public function update(int $id, string $title, string $content): DataResponse public function update(int $id, string $title, string $content): DataResponse

View File

@ -8,12 +8,14 @@ use OCP\AppFramework\Db\Entity;
class MatrixUser extends Entity implements JsonSerializable { class MatrixUser extends Entity implements JsonSerializable {
protected $userId; protected $userId;
protected $matrixUser; protected $matrixUser;
protected $matrixToken;
public function jsonSerialize(): array { public function jsonSerialize(): array {
return [ return [
'id' => $this->id, 'id' => $this->id,
'userId' => $this->userId, 'userId' => $this->userId,
'matrixUser' => $this->matrixUser, 'matrixUser' => $this->matrixUser,
'matrixToken' => $this->matrixToken,
]; ];
} }
} }

View File

@ -22,20 +22,14 @@ class TicketMapper extends QBMapper
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException * @throws DoesNotExistException
*/ */
public function find(int $id, string $userId): MatrixTicket public function find(int $id, MatrixUser $matrixUser): MatrixTicket
{ {
/* @var $qb IQueryBuilder */ /* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from('upschooling_tickets', 't') ->from('upschooling_tickets')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->innerJoin( ->andWhere($qb->expr()->in($matrixUser->getMatrixUser(), ['matrix_assisted_user', 'matrix_helper_user']));
't',
'upschooling_users',
'u',
$qb->expr()->in('u.matrix_user', ['t.matrix_assisted_user', 't.matrix_helper_user'])
)
->andWhere($qb->expr()->eq('u.user_id', $qb->createNamedParameter($userId)));
return $this->findEntity($qb); return $this->findEntity($qb);
} }
@ -43,19 +37,13 @@ class TicketMapper extends QBMapper
* @param string $userId * @param string $userId
* @return array * @return array
*/ */
public function findAll(string $userId): array public function findAll(MatrixUser $matrixUser): array
{ {
/* @var $qb IQueryBuilder */ /* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from('upschooling_users', 'u') ->from('upschooling_tickets')
->where($qb->expr()->eq('u.user_id', $qb->createNamedParameter($userId))) ->where($qb->expr()->in($matrixUser->getMatrixUser(), ['matrix_assisted_user', 'matrix_helper_user']));
->innerJoin(
'u',
'upschooling_tickets',
't',
$qb->expr()->in('u.matrix_user', ['t.matrix_assisted_user', 't.matrix_helper_user'])
);
return $this->findEntities($qb); return $this->findEntities($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

@ -29,22 +29,22 @@ class Version000000Date20210918151800 extends SimpleMigrationStep {
]); ]);
$table->addColumn('matrix_room', 'string', [ $table->addColumn('matrix_room', 'string', [
'notnull' => true, 'notnull' => true,
'length' => 63, 'length' => 200,
]); ]);
$table->addColumn('matrix_assisted_user', 'string', [ $table->addColumn('matrix_assisted_user', 'string', [
'notnull' => true, 'notnull' => true,
'length' => 63, 'length' => 200,
]); ]);
$table->addColumn('matrix_helper_user', 'string', [ $table->addColumn('matrix_helper_user', 'string', [
'notnull' => false, 'notnull' => false,
'length' => 63, 'length' => 200,
]); ]);
// could be optimized by having a "localUser" with foreign key to users.id, // could be optimized by having a "localUser" with foreign key to users.id,
// but that would create consistency issues // but that would create consistency issues
$table->addColumn('status', 'string', [ $table->addColumn('status', 'string', [
'notnull' => true, 'notnull' => true,
'length' => 100, 'length' => 63,
]); ]);
$table->addColumn('version', 'integer', [ $table->addColumn('version', 'integer', [
'notnull' => true, 'notnull' => true,
@ -63,11 +63,15 @@ class Version000000Date20210918151800 extends SimpleMigrationStep {
]); ]);
$table->addColumn('user_id', 'string', [ $table->addColumn('user_id', 'string', [
'notnull' => true, 'notnull' => true,
'length' => 100, 'length' => 200,
]); ]);
$table->addColumn('matrix_user', 'string', [ $table->addColumn('matrix_user', 'string', [
'notnull' => true, 'notnull' => true,
'length' => 63, 'length' => 200,
]);
$table->addColumn('matrix_token', 'string', [
'notnull' => true,
'length' => 200,
]); ]);
$table->setPrimaryKey(['id']); $table->setPrimaryKey(['id']);

View File

@ -3,69 +3,175 @@
namespace OCA\UPschooling\Service; namespace OCA\UPschooling\Service;
use Aryess\PhpMatrixSdk\Exceptions\MatrixException; use Aryess\PhpMatrixSdk\Exceptions\MatrixException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixHttpLibException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixRequestException; use Aryess\PhpMatrixSdk\Exceptions\MatrixRequestException;
use Aryess\PhpMatrixSdk\Exceptions\ValidationException;
use Aryess\PhpMatrixSdk\MatrixClient; use Aryess\PhpMatrixSdk\MatrixClient;
use Aryess\PhpMatrixSdk\Room; use Aryess\PhpMatrixSdk\Room;
use OCA\UPschooling\Db\MatrixUser;
use OCA\UPschooling\Exceptions\RoomNotFoundException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class MatrixService class MatrixService
{ {
/** @var LoggerInterface */ /** @var LoggerInterface */
private $logger; private $logger;
/** @var string */ /** @var MatrixClient */
private $channel; private $client;
/** @var MatrixClient */ /** @var string */
private $client; private $registrationSecret = "~d9fJpPKDZIV67A7=tPCvok:=fTBLV;MFf=9FRxtAazW@-GwSo";
/** @var string */ /**
private $token; * @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);
}
/** @var Room */ /**
private $room; * @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)
);
}
public function __construct(LoggerInterface $logger) { /**
$this->logger = $logger; * @param string $roomId a room id of an existing and joined room.
$this->channel = "#issue:synapse"; * @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property.
$this->client = new MatrixClient("http://synapse:8008"); * @return array the contents of the room state.
$this->token = $this->client->login("upschooling", "secret"); * @throws MatrixException
// try { * @throws RoomNotFoundException
// $this->room = $this->client->createRoom($this->channel, false, array()); */
// } catch (MatrixException $createE) { public function getProperty(string $roomId, string $eventType): array
// try { {
// $this->room = $this->client->joinRoom($this->channel); $this->findRoom($roomId); // make sure the room exists/is joined
// } catch (MatrixException $e) { $content = $this->client->api()->getStateEvent($roomId, $eventType);
// $this->logger->error( $this->logger->debug(
// "Could not create room ".$this->channel, "Got property " . $eventType . " from room " . $roomId,
// array('exception' => $e, 'causedBy' => $createE) array("room" => $roomId, "key" => $eventType, "value" => $content)
// ); );
// } return $content;
// } }
}
/** /**
* Sets a property on a channel. * @param string $roomId a room id of an existing and joined room.
* * @throws RoomNotFoundException
* @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property. * @returns int the origin server timestamp of the most recent event.
* @param array $content the contents as a JSON serializable array. */
* @throws MatrixException public function getLastEventDate(string $roomId): int
*/ {
public function setProperty(string $eventType, array $content) $room = $this->findRoom($roomId);
{ $events = $room->getEvents();
$this->room->sendStateEvent($eventType, $content); 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;
}
}
/** /**
* @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property. * Registers a new matrix user with the local matrix server.
* @return array the contents of the room state. *
* @throws MatrixException|MatrixRequestException * @throws MatrixException
*/ * @throws MatrixHttpLibException
public function getProperty(string $eventType): array * @throws MatrixRequestException
{ * @return MatrixUser user object without id or Nextcloud user id set.
// first parameter should be $this->room->roomId */
return $this->client->api()->getStateEvent($this->channel, $eventType); 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

@ -2,76 +2,113 @@
namespace OCA\UPschooling\Service; namespace OCA\UPschooling\Service;
use Aryess\PhpMatrixSdk\Exceptions\MatrixException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixHttpLibException;
use Aryess\PhpMatrixSdk\Exceptions\MatrixRequestException;
use Exception; use Exception;
use OCA\UPschooling\Db\MatrixTicket; use OCA\UPschooling\Db\MatrixTicket;
use OCA\UPschooling\Db\MatrixUser;
use OCA\UPschooling\Db\TicketMapper; use OCA\UPschooling\Db\TicketMapper;
use OCA\UPschooling\Db\UserMapper;
use OCA\UPschooling\Exceptions\RoomNotFoundException;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\MultipleObjectsReturnedException;
class TicketService { class TicketService {
/** @var TicketMapper */ /** @var MatrixService */
private $mapper;
private $matrix; private $matrix;
public function __construct(MatrixService $matrix, TicketMapper $mapper) { /** @var TicketMapper */
private $ticketMapper;
/** @var UserMapper */
private $userMapper;
public function __construct(MatrixService $matrix, TicketMapper $ticketMapper, UserMapper $userMapper) {
$this->matrix = $matrix; $this->matrix = $matrix;
$this->mapper = $mapper; $this->ticketMapper = $ticketMapper;
$this->userMapper = $userMapper;
} }
public function findAll(string $userId): array { public function findAll(string $userId): array {
return $this->mapper->findAll($userId); $dbTickets = $this->ticketMapper->findAll($this->getOrCreateUser($userId));
return array_map(function ($ticket) { return $this->resolveTicket($ticket); }, $dbTickets);
} }
private function handleException(Exception $e): void { public function find($id, $userId): array {
if ($e instanceof DoesNotExistException || return $this->resolveTicket($this->ticketMapper->find($id, $this->getOrCreateUser($userId)));
$e instanceof MultipleObjectsReturnedException) {
throw new NoteNotFound($e->getMessage());
} else {
throw $e;
}
} }
public function find($id, $userId) { public function create($title, $content, $userId): array {
try { $matrixUser = $this->getOrCreateUser($userId);
return $this->mapper->find($id, $userId); // FIXME: room must be joined on the remote support-platform Nextcloud
$roomId = $this->matrix->createRoom();
// in order to be able to plug in different storage backends like files $this->matrix->setProperty($roomId, "upschooling.ticket", array(
// for instance it is a good idea to turn storage related exceptions // id is to be set from the remote support-platform Nextcloud
// into service related exceptions so controllers and service users "title" => $title,
// have to deal with only one type of exception "description" => $content,
} catch (Exception $e) { ));
$this->handleException($e); $ticket = new MatrixTicket();
} $ticket->setMatrixRoom($roomId);
} $ticket->setMatrixAssistedUser($matrixUser->getMatrixUser());
$ticket->setStatus("open");
public function create($title, $content, $userId) { $ticket->setVersion(1);
$note = new MatrixTicket(); return $this->resolveTicket($this->ticketMapper->insert($ticket));
$note->setTitle($title);
$note->setContent($content);
$note->setUserId($userId);
return $this->mapper->insert($note);
} }
public function update($id, $title, $content, $userId) { public function update($id, $title, $content, $userId) {
try { throw new Exception("Not implemented");
$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) { 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 { try {
$note = $this->mapper->find($id, $userId); $matrixTicketContent = $this->matrix->getProperty($ticket->getMatrixRoom(), "upschooling.ticket");
$this->mapper->delete($note); $ticketId = array_get($matrixTicketContent, "id", $ticket->getId());
return $note; $title = array_get($matrixTicketContent, "title", "Untitled");
} catch (Exception $e) { $description = array_get($matrixTicketContent, "description", "");
$this->handleException($e); $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;
} }
} }
} }

View File

@ -9,7 +9,7 @@ DIR="${0%/*}"
podman rm -if synapse podman rm -if synapse
podman rm -if nextcloud podman rm -if nextcloud
podman run -d --name=nextcloud -p 8080:80 -v "$DIR:/var/www/html/custom_apps/upschooling" docker.io/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 podman exec nextcloud chown -R 33 /var/www/html/custom_apps
"$DIR/podman-reown.sh" "$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 bash -c 'cd /var/www/html/custom_apps/upschooling && make composer'

View File

@ -38,6 +38,7 @@ export default {
*/ */
currentTicket: undefined, currentTicket: undefined,
ticketsFetched: false,
tickets: [], tickets: [],
} }
}, },
@ -45,19 +46,43 @@ export default {
$route: 'fetchTickets', $route: 'fetchTickets',
}, },
created() { created() {
this.createExampleContent()
this.fetchTickets() this.fetchTickets()
}, },
methods: { methods: {
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)
}
}).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() { fetchTickets() {
axios.get( axios.get(
'api/v1/tickets', 'api/v1/tickets',
{ headers: { Accept: 'application/json' } } { headers: { Accept: 'application/json' } }
).then((response) => { ).then((response) => {
if (Array.isArray(response.data)) { if (Array.isArray(response.data)) {
this.ticketsFetched = true
if (response.data.length !== 0) { if (response.data.length !== 0) {
this.tickets.push(response.data) this.tickets = response.data
console.debug(this.tickets) // FIXME
} else { } else {
console.warn('Empty ticket list :(') console.debug('Empty ticket list :(')
} }
} else { } else {
console.error('API did not return array: ', response) console.error('API did not return array: ', response)
@ -69,7 +94,7 @@ export default {
console.debug('upschooling', 'saveTicket', ticketId, data) console.debug('upschooling', 'saveTicket', ticketId, data)
}, },
openTicket(ticketId) { openTicket(ticketId) {
this.currentTicket = this.tickets.find((obj) => obj.id === ticketId) this.currentTicket = this.tickets.find((obj) => obj.ticketId === ticketId)
}, },
deselectTicket() { deselectTicket() {
this.currentTicket = null this.currentTicket = null

View File

@ -9,13 +9,13 @@
</button> </button>
</div> </div>
<h2>Ticket "ding"</h2> <h2>Ticket "ding"</h2>
<KeyValueTable :data-rows="{Name: ticket.title, Status: ticket.status, Ablaufdatum: '<<gestern>>'}" /> <KeyValueTable :data-rows="{Id: ticket.ticketId, Name: ticket.title, Status: ticket.status, Geaendert: toLocaleDate(ticket.lastModified)}" />
<br> <br>
<label for="description">{{ t('upschooling', 'Beschreibung') }}</label> <label for="description">{{ t('upschooling', 'Beschreibung') }}</label>
<textarea id="description" v-model.trim.lazy="description" rows="15" /> <textarea id="description" v-model.trim.lazy="description" rows="15" />
<br> <br>
<button @click="save"> <button @click="save">
Speichern {{ t('upschooling', 'Speichern') }}
</button> </button>
<hr> <hr>
<div class="placeholder" /> <div class="placeholder" />
@ -41,8 +41,13 @@ export default {
} }
}, },
methods: { methods: {
toLocaleDate(timestamp) {
const date = new Date(timestamp)
return date.toLocaleString()
},
save() { save() {
this.$emit('save-ticket', this.ticket.id, {}) // TODO: give it only the changed data this.$emit('save-ticket', this.ticket.ticketId, {}) // TODO: give it only the changed data
}, },
back() { back() {

View File

@ -7,32 +7,32 @@
<th id="headerName" class="column-name"> <th id="headerName" class="column-name">
<div id="headerName-container"> <div id="headerName-container">
<a class="name sort columntitle" data-sort="name"> <a class="name sort columntitle" data-sort="name">
<span>Titel (#Ticket-Nummer)</span> <span>{{ t('upschooling', 'Titel (#Ticket-Nummer)') }}</span>
<span class="sort-indicator icon-triangle-n" /> <span class="sort-indicator icon-triangle-n" />
</a> </a>
</div> </div>
</th> </th>
<th id="headerStatus" class="column-status"> <th id="headerStatus" class="column-status">
<a class="status sort columntitle" data-sort="status"> <a class="status sort columntitle" data-sort="status">
<span>Status</span> <span>{{ t('upschooling', 'Status') }}</span>
<span class="sort-indicator hidden icon-triangle-s" /> <span class="sort-indicator hidden icon-triangle-s" />
</a> </a>
</th> </th>
<th id="headerDate" class="column-mtime"> <th id="headerDate" class="column-mtime">
<a id="modified" class="columntitle" data-sort="mtime"> <a id="modified" class="columntitle" data-sort="mtime">
<span>Geändert</span> <span>{{ t('upschooling', 'Zuletzt Geändert') }}</span>
<span class="sort-indicator hidden icon-triangle-s" /> <span class="sort-indicator hidden icon-triangle-s" />
</a> </a>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody id="tickettbody"> <tbody id="tickettbody">
<tr v-for="item in tickets" :key="item.id"> <tr v-for="item in tickets" :key="item.ticketId">
<td class="filename ui-draggable ui-draggable-handle"> <td class="filename ui-draggable ui-draggable-handle">
<a class="name" :href="'#ticket-' + item.id" @click="openTicket(item.id)"> <a class="name" :href="'#ticket-' + item.ticketId" @click="openTicket(item.ticketId)">
<span class="nametext"> <span class="nametext">
<span class="innernametext">{{ item.title }}</span> <span class="innernametext">{{ item.title }}</span>
<span class="ticket-number"> (<span class="icon icon-ticket" />#{{ item.id }})</span> <span class="ticket-number"> (<span class="icon icon-ticket" />#{{ item.ticketId }})</span>
</span> </span>
</a> </a>
</td> </td>
@ -42,11 +42,10 @@
<td class="date"> <td class="date">
<span <span
class="modified live-relative-timestamp" class="modified live-relative-timestamp"
title="" :title="toLocaleDate(item.lastModified)"
data-timestamp="1628157115000" :data-timestamp="item.lastModified"
style="color:rgb(81,81,81)" style="color:rgb(81,81,81)">
data-original-title="5. August 2021 11:51"> {{ toLocaleDate(item.lastModified) }}
vor 16 Tagen <!-- ToDo -->
</span> </span>
</td> </td>
</tr> </tr>
@ -66,6 +65,10 @@ export default {
}, },
}, },
methods: { methods: {
toLocaleDate(timestamp) {
const date = new Date(timestamp)
return date.toLocaleString()
},
openTicket(ticketId) { openTicket(ticketId) {
this.$emit('open-ticket', ticketId) this.$emit('open-ticket', ticketId)
}, },

View File

@ -4,8 +4,8 @@ namespace Unit\Service;
use OCA\UPschooling\Db\MatrixTicket; use OCA\UPschooling\Db\MatrixTicket;
use OCA\UPschooling\Db\TicketMapper; use OCA\UPschooling\Db\TicketMapper;
use OCA\UPschooling\Db\UserMapper;
use OCA\UPschooling\Service\MatrixService; use OCA\UPschooling\Service\MatrixService;
use OCA\UPschooling\Service\NoteNotFound;
use OCA\UPschooling\Service\TicketService; use OCA\UPschooling\Service\TicketService;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -20,17 +20,23 @@ class NoteServiceTest extends TestCase {
private $matrixService; private $matrixService;
/** @var TicketMapper */ /** @var TicketMapper */
private $mapper; private $ticketMapper;
/** @var UserMapper */
private $userMapper;
/** @var string */ /** @var string */
private $userId = 'john'; private $userId = 'john';
public function setUp(): void { public function setUp(): void {
$this->mapper = $this->getMockBuilder(TicketMapper::class) $this->ticketMapper = $this->getMockBuilder(TicketMapper::class)
->disableOriginalConstructor()
->getMock();
$this->userMapper = $this->getMockBuilder(UserMapper::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$this->matrixService = new MatrixService(new NullLogger()); $this->matrixService = new MatrixService(new NullLogger());
$this->service = new TicketService($this->matrixService, $this->mapper); $this->service = new TicketService($this->matrixService, $this->ticketMapper, $this->userMapper);
} }
public function testUpdate() { public function testUpdate() {
@ -40,7 +46,7 @@ class NoteServiceTest extends TestCase {
'title' => 'yo', 'title' => 'yo',
'content' => 'nope' 'content' => 'nope'
]); ]);
$this->mapper->expects($this->once()) $this->ticketMapper->expects($this->once())
->method('find') ->method('find')
->with($this->equalTo(3)) ->with($this->equalTo(3))
->will($this->returnValue($note)); ->will($this->returnValue($note));
@ -49,7 +55,7 @@ class NoteServiceTest extends TestCase {
$updatedNote = MatrixTicket::fromRow(['id' => 3]); $updatedNote = MatrixTicket::fromRow(['id' => 3]);
$updatedNote->setTitle('title'); $updatedNote->setTitle('title');
$updatedNote->setContent('content'); $updatedNote->setContent('content');
$this->mapper->expects($this->once()) $this->ticketMapper->expects($this->once())
->method('update') ->method('update')
->with($this->equalTo($updatedNote)) ->with($this->equalTo($updatedNote))
->will($this->returnValue($updatedNote)); ->will($this->returnValue($updatedNote));
@ -60,9 +66,9 @@ class NoteServiceTest extends TestCase {
} }
public function testUpdateNotFound() { public function testUpdateNotFound() {
$this->expectException(NoteNotFound::class); // $this->expectException(NoteNotFound::class);
// test the correct status code if no note is found // test the correct status code if no note is found
$this->mapper->expects($this->once()) $this->ticketMapper->expects($this->once())
->method('find') ->method('find')
->with($this->equalTo(3)) ->with($this->equalTo(3))
->will($this->throwException(new DoesNotExistException(''))); ->will($this->throwException(new DoesNotExistException('')));