Merge branch 'finns-dungeon' into experimental

This commit is contained in:
Ben 2021-09-18 10:40:25 +02:00
commit a7cb94d136
Signed by: ben
GPG key ID: 0F54A7ED232D3319
14 changed files with 192 additions and 254 deletions

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 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 chown -R 33 /var/www/html/custom_apps
docker exec nextcloud chmod -R ug+rw /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 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 config:system:set --value=true --type=boolean debug
docker exec --user 33 nextcloud php occ app:enable --force upschooling docker exec --user 33 nextcloud php occ app:enable --force upschooling

View file

@ -3,13 +3,13 @@
namespace OCA\UPschooling\Controller; namespace OCA\UPschooling\Controller;
use OCA\UPschooling\AppInfo\Application; use OCA\UPschooling\AppInfo\Application;
use OCA\UPschooling\Service\NoteService; use OCA\UPschooling\Service\TicketService;
use OCP\AppFramework\ApiController; use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest; use OCP\IRequest;
class NoteApiController extends ApiController { class TicketApiController extends ApiController {
/** @var NoteService */ /** @var TicketService */
private $service; private $service;
/** @var string */ /** @var string */
@ -18,7 +18,7 @@ class NoteApiController extends ApiController {
use Errors; use Errors;
public function __construct(IRequest $request, public function __construct(IRequest $request,
NoteService $service, TicketService $service,
$userId) { $userId) {
parent::__construct(Application::APP_ID, $request); parent::__construct(Application::APP_ID, $request);
$this->service = $service; $this->service = $service;

View file

@ -3,13 +3,13 @@
namespace OCA\UPschooling\Controller; namespace OCA\UPschooling\Controller;
use OCA\UPschooling\AppInfo\Application; use OCA\UPschooling\AppInfo\Application;
use OCA\UPschooling\Service\NoteService; use OCA\UPschooling\Service\TicketService;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest; use OCP\IRequest;
class NoteController extends Controller { class TicketController extends Controller {
/** @var NoteService */ /** @var TicketService */
private $service; private $service;
/** @var string */ /** @var string */
@ -18,7 +18,7 @@ class NoteController extends Controller {
use Errors; use Errors;
public function __construct(IRequest $request, public function __construct(IRequest $request,
NoteService $service, TicketService $service,
$userId) { $userId) {
parent::__construct(Application::APP_ID, $request); parent::__construct(Application::APP_ID, $request);
$this->service = $service; $this->service = $service;

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
];
}
}

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

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

View file

@ -8,19 +8,19 @@ use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection; use OCP\IDBConnection;
class NoteMapper extends QBMapper { class TicketMapper extends QBMapper {
public function __construct(IDBConnection $db) { public function __construct(IDBConnection $db) {
parent::__construct($db, 'upschooling', Note::class); parent::__construct($db, 'upschooling', Ticket::class);
} }
/** /**
* @param int $id * @param int $id
* @param string $userId * @param string $userId
* @return Entity|Note * @return Entity|Ticket
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException * @throws DoesNotExistException
*/ */
public function find(int $id, string $userId): Note { public function find(int $id, string $userId): Ticket {
/* @var $qb IQueryBuilder */ /* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')

View file

@ -7,15 +7,15 @@ use Exception;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCA\UPschooling\Db\Note; use OCA\UPschooling\Db\Ticket;
use OCA\UPschooling\Db\NoteMapper; use OCA\UPschooling\Db\TicketMapper;
class NoteService { class TicketService {
/** @var NoteMapper */ /** @var TicketMapper */
private $mapper; private $mapper;
public function __construct(NoteMapper $mapper) { public function __construct(TicketMapper $mapper) {
$this->mapper = $mapper; $this->mapper = $mapper;
} }
@ -46,7 +46,7 @@ class NoteService {
} }
public function create($title, $content, $userId) { public function create($title, $content, $userId) {
$note = new Note(); $note = new Ticket();
$note->setTitle($title); $note->setTitle($title);
$note->setContent($content); $note->setContent($content);
$note->setUserId($userId); $note->setUserId($userId);

View file

@ -1,215 +1,25 @@
<template> <template>
<div id="content" class="app-upschooling"> <div id="content" class="app-upschooling">
<AppNavigation> <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>
<AppContent> <AppContent>
<div v-if="currentNote"> <Ticket />
<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>
<div v-else id="emptycontent">
<div class="icon-file" />
<h2>{{ t('upschooling', 'Create a note to get started') }}</h2>
</div>
</AppContent> </AppContent>
</div> </div>
</template> </template>
<script> <script>
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import AppContent from '@nextcloud/vue/dist/Components/AppContent' import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation' 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 '@nextcloud/dialogs/styles/toast.scss' import '@nextcloud/dialogs/styles/toast.scss'
import { generateUrl } from '@nextcloud/router' import Ticket from './Ticket'
import { showError, showSuccess } from '@nextcloud/dialogs'
import axios from '@nextcloud/axios'
export default { export default {
name: 'App', name: 'App',
components: { components: {
ActionButton, Ticket,
AppContent, AppContent,
AppNavigation, AppNavigation,
AppNavigationItem,
AppNavigationNew,
},
data() {
return {
notes: [],
currentNoteId: null,
updating: false,
loading: true,
}
},
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 !== ''
},
},
/**
* 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
},
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
}
showSuccess(t('upschooling', 'Note deleted'))
} catch (e) {
console.error(e)
showError(t('upschooling', 'Could not delete the note'))
}
},
}, },
} }
</script> </script>

59
src/Ticket.vue Normal file
View file

@ -0,0 +1,59 @@
<template>
<div class="single-ticket">
<div class="header-bar">
<button>
{{ t('upschooling', 'Ticket Schließen') }}
</button>
<button>
{{ t('upschooling', 'Speichern') }}
</button>
</div>
<h2>Ticket "ding"</h2>
<KeyValueTable :data-rows="{Name: 'Bernd', Status: 'Offen', Ablaufdatum: 'gestern'}" />
<br>
<label for="description">{{ t('upschooling', 'Beschreibung') }}</label>
<textarea id="description" v-model.trim.lazy="description" rows="15" />
<br>
<button @click="save">
Speichern
</button>
<hr>
<div class="placeholder" />
</div>
</template>
<script>
import KeyValueTable from './components/KeyValueTable'
export default {
name: 'Ticket',
components: { KeyValueTable },
data() {
return {
description: '',
}
},
methods: {
save() {},
},
}
</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,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

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

View file

@ -2,11 +2,11 @@
namespace OCA\UPschooling\Tests\Unit\Controller; namespace OCA\UPschooling\Tests\Unit\Controller;
use OCA\UPschooling\Controller\NoteApiController; use OCA\UPschooling\Controller\TicketApiController;
class NoteApiControllerTest extends NoteControllerTest { class NoteApiControllerTest extends NoteControllerTest {
public function setUp(): void { public function setUp(): void {
parent::setUp(); parent::setUp();
$this->controller = new NoteApiController($this->request, $this->service, $this->userId); $this->controller = new TicketApiController($this->request, $this->service, $this->userId);
} }
} }

View file

@ -8,8 +8,8 @@ use OCP\AppFramework\Http;
use OCP\IRequest; use OCP\IRequest;
use OCA\UPschooling\Service\NoteNotFound; use OCA\UPschooling\Service\NoteNotFound;
use OCA\UPschooling\Service\NoteService; use OCA\UPschooling\Service\TicketService;
use OCA\UPschooling\Controller\NoteController; use OCA\UPschooling\Controller\TicketController;
class NoteControllerTest extends TestCase { class NoteControllerTest extends TestCase {
protected $controller; protected $controller;
@ -19,10 +19,10 @@ class NoteControllerTest extends TestCase {
public function setUp(): void { public function setUp(): void {
$this->request = $this->getMockBuilder(IRequest::class)->getMock(); $this->request = $this->getMockBuilder(IRequest::class)->getMock();
$this->service = $this->getMockBuilder(NoteService::class) $this->service = $this->getMockBuilder(TicketService::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$this->controller = new NoteController($this->request, $this->service, $this->userId); $this->controller = new TicketController($this->request, $this->service, $this->userId);
} }
public function testUpdate() { public function testUpdate() {

View file

@ -7,9 +7,9 @@ use PHPUnit\Framework\TestCase;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCA\UPschooling\Db\Note; use OCA\UPschooling\Db\Ticket;
use OCA\UPschooling\Service\NoteService; use OCA\UPschooling\Service\TicketService;
use OCA\UPschooling\Db\NoteMapper; use OCA\UPschooling\Db\TicketMapper;
class NoteServiceTest extends TestCase { class NoteServiceTest extends TestCase {
private $service; private $service;
@ -17,15 +17,15 @@ class NoteServiceTest extends TestCase {
private $userId = 'john'; private $userId = 'john';
public function setUp(): void { public function setUp(): void {
$this->mapper = $this->getMockBuilder(NoteMapper::class) $this->mapper = $this->getMockBuilder(TicketMapper::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$this->service = new NoteService($this->mapper); $this->service = new TicketService($this->mapper);
} }
public function testUpdate() { public function testUpdate() {
// the existing note // the existing note
$note = Note::fromRow([ $note = Ticket::fromRow([
'id' => 3, 'id' => 3,
'title' => 'yo', 'title' => 'yo',
'content' => 'nope' 'content' => 'nope'
@ -36,7 +36,7 @@ class NoteServiceTest extends TestCase {
->will($this->returnValue($note)); ->will($this->returnValue($note));
// the note when updated // the note when updated
$updatedNote = Note::fromRow(['id' => 3]); $updatedNote = Ticket::fromRow(['id' => 3]);
$updatedNote->setTitle('title'); $updatedNote->setTitle('title');
$updatedNote->setContent('content'); $updatedNote->setContent('content');
$this->mapper->expects($this->once()) $this->mapper->expects($this->once())