Skip to content
Snippets Groups Projects
Commit 7293abba authored by Ron Lucke's avatar Ron Lucke
Browse files

StEP #2472

Merge request studip/studip!2296
parent 874bd358
No related branches found
No related tags found
No related merge requests found
Showing
with 384 additions and 29 deletions
......@@ -7,19 +7,27 @@ use Psr\Http\Message\ResponseInterface as Response;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\JsonApiController;
use JsonApi\Schemas\FeedbackEntry as FeedbackEntrySchema;
/**
* Displays a certain feedback entry.
*/
class FeedbackEntriesShow extends JsonApiController
{
protected $allowedIncludePaths = ['author', 'feedback-element'];
protected $allowedIncludePaths = [FeedbackEntrySchema::REL_AUTHOR, FeedbackEntrySchema::REL_FEEDBACK];
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @SuppressWarnings(PHPMD.StaticAccess)
*
* @param array $args
*
* @return Response
*/
public function __invoke(Request $request, Response $response, $args)
{
if (!$resource = \FeedbackEntry::find($args['id'])) {
$resource = \FeedbackEntry::find($args['id']);
if (!$resource) {
throw new RecordNotFoundException();
}
......
<?php
namespace JsonApi\Routes\Feedback;
use FeedbackElement;
use FeedbackEntry;
use User;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\JsonApiController;
use JsonApi\Routes\ValidationTrait;
use JsonApi\Schemas\FeedbackElement as FeedbackElementSchema;
use JsonApi\Schemas\FeedbackEntry as FeedbackEntrySchema;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Update a FeedbackEntry.
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/
class FeedbackEntriesUpdate extends JsonApiController
{
use RatingHelper;
use ValidationTrait;
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @param array $args
*
* @return Response
*/
public function __invoke(Request $request, Response $response, $args)
{
$resource = \FeedbackEntry::find($args['id']);
if (!$resource) {
throw new RecordNotFoundException();
}
$json = $this->validate($request);
$user = $this->getUser($request);
if (!Authority::canUpdateFeedbackEntry($user, $resource)) {
throw new AuthorizationFailedException();
}
$feedbackEntry = $this->update($resource, $json);
return $this->getContentResponse($feedbackEntry);
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameters)
*
* @param array $json
* @param mixed $data
*
* @return string|void
*/
protected function validateResourceDocument($json, $data)
{
if (!self::arrayHas($json, 'data')) {
return 'Missing `data` member at document´s top level.';
}
if (FeedbackEntrySchema::TYPE !== self::arrayGet($json, 'data.type')) {
return 'Invalid `type` of document´s `data`.';
}
if (!self::arrayHas($json, 'data.id')) {
return 'An existing document must have an `id`.';
}
$required = ['rating'];
foreach ($required as $attribute) {
if (!self::arrayHas($json, 'data.attributes.' . $attribute)) {
return 'Missing `' . $attribute . '` attribute.';
}
}
}
private function update(FeedbackEntry $feedbackEntry, array $json): FeedbackEntry
{
$feedbackEntry->rating = $this->getRating(
$feedbackEntry->feedback,
(int) self::arrayGet($json, 'data.attributes.rating')
);
if ($feedbackEntry->feedback->commentable && self::arrayHas($json, 'data.attributes.comment')) {
$feedbackEntry->comment = self::arrayGet($json, 'data.attributes.comment');
}
$feedbackEntry->anonymous = (int) self::arrayGet($json, 'data.attributes.anonymous');
$feedbackEntry->store();
return $feedbackEntry;
}
}
<?php
namespace JsonApi\Routes\Feedback;
use FeedbackRange;
use SimpleORMap;
trait RangeTypeAware
{
protected $possibleRangeTypes = null;
protected function preparePossibleRangeTypes(): void
{
foreach (app('json-api-integration-schemas') as $class => $schema) {
if (is_subclass_of($class, FeedbackRange::class) && is_subclass_of($class, SimpleORMap::class)) {
$this->possibleRangeTypes[$schema::TYPE] = $class;
}
}
}
}
<?php
namespace JsonApi\Routes\Feedback;
use FeedbackElement;
trait RatingHelper
{
private function getRating(FeedbackElement $element, int $rating): int
{
$mode = intval($element['mode']);
if ($mode === 0) {
return 0;
}
if ($rating === 0) {
return 1;
}
if ($mode === 1) {
return min(5, $rating);
}
if ($mode === 2) {
return min(10, $rating);
}
throw new InvalidArgumentException("Invalid mode {$mode}");
}
}
......@@ -40,6 +40,8 @@ class Instance extends SchemaProvider
'root-layout' => $resource->getRootLayout(),
'sequential-progression' => $resource->getSequentialProgression(),
'editing-permission-level' => $resource->getEditingPermissionLevel(),
'show-feedback-popup' => $resource->getShowFeedbackPopup(),
'show-feedback-in-contentbar' => $resource->getShowFeedbackInContentbar(),
'certificate-settings' => $resource->getCertificateSettings(),
'reminder-settings' => $resource->getReminderSettings(),
'reset-progress-settings' => $resource->getResetProgressSettings(),
......
......@@ -24,6 +24,7 @@ class StructuralElement extends SchemaProvider
const REL_USER = 'user';
const REL_TASK = 'task';
const REL_UNIT = 'unit';
const REL_FEEDBACKELEMENT = 'feedback-element';
/**
* {@inheritdoc}
......@@ -140,6 +141,12 @@ class StructuralElement extends SchemaProvider
$this->shouldInclude($context, self::REL_UNIT)
);
$relationships = $this->addFeedbackElementRelationship(
$relationships,
$resource,
$this->shouldInclude($context, self::REL_FEEDBACKELEMENT)
);
return $relationships;
}
......@@ -380,6 +387,22 @@ class StructuralElement extends SchemaProvider
return $relationships;
}
private function addFeedbackElementRelationship(array $relationships, $resource, $includeData): array
{
$relation = [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_FEEDBACKELEMENT),
],
];
$feedback = $resource->getFeedbackElement();
$relation[self::RELATIONSHIP_DATA] = $feedback;
$relationships[self::REL_FEEDBACKELEMENT] = $relation;
return $relationships;
}
private static $memo = [];
private function createLinkToCourse($rangeId)
......
......@@ -13,6 +13,7 @@ class Unit extends SchemaProvider
const REL_CREATOR= 'creator';
const REL_RANGE = 'range';
const REL_STRUCTURAL_ELEMENT = 'structural-element';
const REL_FEEDBACK_ELEMENT = 'feedback-element';
/**
* {@inheritdoc}
......@@ -75,6 +76,16 @@ class Unit extends SchemaProvider
]
: [self::RELATIONSHIP_DATA => null];
$feedback = $resource->getFeedbackElement();
$relationships[self::REL_FEEDBACK_ELEMENT] = $feedback
? [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->createLinkToResource($feedback),
],
self::RELATIONSHIP_DATA => $feedback,
]
: [self::RELATIONSHIP_DATA => null];
return $relationships;
}
}
......@@ -2,24 +2,28 @@
namespace JsonApi\Schemas;
use JsonApi\Errors\InternalServerError;
use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
use Neomerx\JsonApi\Schema\Link;
class FeedbackElement extends SchemaProvider
{
const TYPE = 'feedback-elements';
const REL_AUTHOR = 'author';
const REL_COURSE = 'course';
const REL_ENTRIES = 'entries';
const REL_RANGE = 'range';
public const TYPE = 'feedback-elements';
public const REL_AUTHOR = 'author';
public const REL_COURSE = 'course';
public const REL_ENTRIES = 'entries';
public const REL_RANGE = 'range';
public function getId($resource): ?string
{
return (int) $resource->id;
return (string) $resource->id;
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function getAttributes($resource, ContextInterface $context): iterable
{
$attributes = [
......@@ -28,6 +32,9 @@ class FeedbackElement extends SchemaProvider
'mode' => (int) $resource['mode'],
'results-visible' => (bool) $resource['results_visible'],
'is-commentable' => (bool) $resource['commentable'],
'anonymous-entries' => (bool) $resource['anonymous_entries'],
'average-rating' => $resource->getAverageRating(),
'has-entries' => $resource->hasEntries(),
'mkdate' => date('c', $resource['mkdate']),
'chdate' => date('c', $resource['chdate'])
......@@ -76,7 +83,7 @@ class FeedbackElement extends SchemaProvider
return $relationships;
}
private function getAuthorRelationship(array $relationships, \FeedbackElement $resource, $includeData): array
private function getAuthorRelationship(array $relationships, \FeedbackElement $resource, bool $includeData): array
{
$userId = $resource['user_id'];
$related = $includeData ? \User::find($userId) : \User::build(['id' => $userId], false);
......@@ -90,7 +97,7 @@ class FeedbackElement extends SchemaProvider
return $relationships;
}
private function getCourseRelationship(array $relationships, \FeedbackElement $resource, $includeData): array
private function getCourseRelationship(array $relationships, \FeedbackElement $resource, bool $includeData): array
{
if ($courseId = $resource['course_id']) {
$related = $includeData ? \Course::find($courseId) : \Course::build(['id' => $courseId], false);
......@@ -119,23 +126,17 @@ class FeedbackElement extends SchemaProvider
private function getRangeRelationship(array $relationships, \FeedbackElement $resource, bool $includeData): array
{
$rangeType = $resource['range_type'];
$link = null;
$range = $resource->getRange();
try {
$link = $this->createLinkToResource($rangeType);
if (
is_subclass_of($rangeType, \FeedbackRange::class) &&
is_subclass_of($rangeType, \SimpleORMap::class)
) {
if ($range = $rangeType::find($resource['range_id'])) {
$link = $this->createLinkToResource($range);
$relationships[self::REL_RANGE] = [
self::RELATIONSHIP_LINKS => [Link::RELATED => $link],
self::RELATIONSHIP_DATA => $range
];
}
}
} catch (\InvalidArgumentException $e) {
// don't show this relation
} catch (InternalServerError $ise) {
// don't show this relation
}
return $relationships;
......
......@@ -21,6 +21,7 @@ class FeedbackEntry extends SchemaProvider
$attributes = [
'comment' => (string) $resource['comment'],
'rating' => 0 === $resource->feedback->mode ? null : $resource['rating'],
'anonymous' => (bool) $resource['anonymous'],
'mkdate' => date('c', $resource['mkdate']),
'chdate' => date('c', $resource['chdate']),
];
......
......@@ -174,6 +174,14 @@ class Instance
\UserConfig::get($user->id)->store('COURSEWARE_FAVORITE_BLOCK_TYPES', $favorites);
}
/*
*
* GENERAL SETTINGS
*
*/
/**
* Returns which layout is set for root node of this coursware instance
*
......@@ -287,6 +295,43 @@ class Instance
}
/*
*
* FEEDBACK
*
*/
public function getShowFeedbackPopup(): bool
{
$showFeedbackPopup = $this->unit->config['show_feedback_popup'] ?? false;
return (bool) $showFeedbackPopup;
}
public function setShowFeedbackPopup(bool $showFeedbackPopup): void
{
$this->unit->config['show_feedback_popup'] = $showFeedbackPopup ? 1 : 0;
}
public function getShowFeedbackInContentbar(): bool
{
$showFeedbackInContentbar = $this->unit->config['show_feedback__in_contentbar'] ?? false;
return (bool) $showFeedbackInContentbar;
}
public function setShowFeedbackInContentbar(bool $showFeedbackInContentbar): void
{
$this->unit->config['show_feedback__in_contentbar'] = $showFeedbackInContentbar ? 1 : 0;
}
/*
*
* CERTIFICATE
*
*/
/**
* Returns the certificate creation settings.
*
......
......@@ -53,7 +53,7 @@ use User;
* @property Task $task has_one Task
* @property mixed $image additional field
*/
class StructuralElement extends \SimpleORMap implements \PrivacyObject
class StructuralElement extends \SimpleORMap implements \PrivacyObject, \FeedbackRange
{
protected static function configure($config = [])
{
......@@ -151,6 +151,7 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject
if (is_a($image, \FileRef::class)) {
$image->delete();
}
\FeedbackElement::deleteBySQL('range_id = ? AND range_type = ?', [$this->id, self::class]);
}
/**
......@@ -1195,4 +1196,48 @@ SQL;
]);
}
}
public function getRangeCourseId(): string
{
return $this->range_id;
}
public function getRangeName(): string
{
return $this->title;
}
public function getRangeIcon($role): string
{
return \Icon::create('courseware', $role);
}
public function getRangeUrl(): string
{
$unit = $this->findUnit();
if ($this->range_type === 'user') {
return 'contents/courseware/courseware/' . $unit->id . '#/structural_element/' . $this->id;
}
return 'course/courseware/courseware/' . $unit->id . '?cid=' . $this->range_id . '#/structural_element/' . $this->id;
}
public function isRangeAccessible(string $user_id = null): bool
{
$user = \User::find($user_id);
if ($user) {
return $this->canRead($user);
}
return false;
}
public function getFeedbackElement()
{
return \FeedbackElement::findOneBySQL(
'range_id = ? AND range_type = ?',
[$this->id, self::class]
);
}
}
......@@ -31,7 +31,7 @@ use User;
* @property StructuralElement $structural_element has_one StructuralElement
*/
class Unit extends \SimpleORMap implements \PrivacyObject
class Unit extends \SimpleORMap implements \PrivacyObject, \FeedbackRange
{
protected static function configure($config = [])
{
......@@ -60,10 +60,16 @@ class Unit extends \SimpleORMap implements \PrivacyObject
];
$config['registered_callbacks']['after_delete'][] = 'updatePositionsAfterDelete';
$config['registered_callbacks']['before_delete'][] = 'cbBeforeDelete';
parent::configure($config);
}
public function cbBeforeDelete()
{
\FeedbackElement::deleteBySQL('range_id = ? AND range_type = ?', [$this->id, self::class]);
}
public static function findCoursesUnits(\Course $course): array
{
return self::findBySQL('range_id = ? AND range_type = ?', [$course->id, 'course']);
......@@ -201,4 +207,46 @@ class Unit extends \SimpleORMap implements \PrivacyObject
return $struct;
}
public function getRangeCourseId(): string
{
return $this->range_id;
}
public function getRangeName(): string
{
return $this->structural_element->title;
}
public function getRangeIcon($role): string
{
return \Icon::create('content2', $role);
}
public function getRangeUrl(): string
{
if ($this->structural_element->range_type === 'user') {
return 'contents/courseware/';
}
return 'course/courseware/' . '?cid=' . $this->range_id;
}
public function isRangeAccessible(string $user_id = null): bool
{
$user = \User::find($user_id);
if ($user) {
return $this->canRead($user);
}
return false;
}
public function getFeedbackElement()
{
return \FeedbackElement::findOneBySQL(
'range_id = ? AND range_type = ?',
[$this->id, self::class]
);
}
}
......@@ -3,6 +3,7 @@
/**
*
* @author Nils Gehrke <nils.gehrke@uni-goettingen.de>
* @author Ron Lucke <lucke@elan-ev.de>
*
* The column "range_type" represents the name of a class that implements
* FeedbackRange.
......@@ -17,6 +18,7 @@
* @property int $mode database column
* @property int $results_visible database column
* @property int $commentable database column
* @property int $anonymous_entries database column
* @property int $mkdate database column
* @property int $chdate database column
* @property SimpleORMapCollection|FeedbackEntry[] $entries has_many FeedbackEntry
......@@ -156,6 +158,22 @@ class FeedbackElement extends SimpleORMap
}
}
public function getAverageRating(): float
{
$ratings = $this->getRatings();
if (empty($ratings)) {
return 0;
}
return array_sum($ratings) / count($ratings);
}
public function hasEntries(): bool
{
return count($this->getRatings()) > 0;
}
public function getRange()
{
return $this->range_type::find($this->range_id);
......
......@@ -3,12 +3,14 @@
/**
*
* @author Nils Gehrke <nils.gehrke@uni-goettingen.de>
* @author Ron Lucke <lucke@elan-ev.de>
*
* @property int $id database column
* @property int $feedback_id database column
* @property string $user_id database column
* @property string $comment database column
* @property int $rating database column
* @property int $anonymous database column
* @property int $mkdate database column
* @property int $chdate database column
* @property FeedbackElement $feedback belongs_to FeedbackElement
......
......@@ -73,7 +73,7 @@ class CoursewareModule extends CorePlugin implements SystemPlugin, StudipModule
);
$navigation->addSubNavigation(
'comments',
new Navigation(_('Kommentare und Feedback'), 'dispatch.php/course/courseware/comments_overview?cid=' . $courseId)
new Navigation(_('Kommentare und Anmerkungen'), 'dispatch.php/course/courseware/comments_overview?cid=' . $courseId)
);
return ['courseware' => $navigation];
......
<svg data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="none" d="M0 0h64v64H0z" data-name="Viewbox 64x64"/><path d="M53.48 7.54H10.53c-3.59 0-6.52 2.94-6.52 6.52v25.45c0 3.59 2.94 6.52 6.52 6.52h8.65l.03 10.5c6.46-.02 11.93-4.5 13.5-10.5h20.78c3.59 0 6.52-2.94 6.52-6.52V14.06c0-3.59-2.94-6.52-6.52-6.52Zm3.02 31.98c0 1.67-1.36 3.02-3.02 3.02H29.67c0 4.55-2.92 8.44-6.98 9.89l-.02-9.89H10.53c-1.67 0-3.02-1.36-3.02-3.02V14.06c0-1.67 1.36-3.02 3.02-3.02h42.95c1.67 0 3.02 1.36 3.02 3.02v25.45Z"/><path d="M37.35 29.74 46 23.45H35.3L32 13.26l-3.3 10.19H18l8.65 6.29-3.3 10.19 8.65-6.3 8.65 6.3-3.3-10.19z"/></svg>
\ No newline at end of file
<svg data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="none" d="M0 0h64v64H0z" data-name="Viewbox 64x64"/><g fill="#28497c"><path d="M53.48 7.54H10.53c-3.59 0-6.52 2.94-6.52 6.52v25.45c0 3.59 2.94 6.52 6.52 6.52h8.65l.03 10.5c6.46-.02 11.93-4.5 13.5-10.5h20.78c3.59 0 6.52-2.94 6.52-6.52V14.06c0-3.59-2.94-6.52-6.52-6.52Zm3.02 31.98c0 1.67-1.36 3.02-3.02 3.02H29.67c0 4.55-2.92 8.44-6.98 9.89l-.02-9.89H10.53c-1.67 0-3.02-1.36-3.02-3.02V14.06c0-1.67 1.36-3.02 3.02-3.02h42.95c1.67 0 3.02 1.36 3.02 3.02v25.45Z"/><path d="M37.35 29.74 46 23.45H35.3L32 13.26l-3.3 10.19H18l8.65 6.29-3.3 10.19 8.65-6.3 8.65 6.3-3.3-10.19z"/></g></svg>
\ No newline at end of file
<svg data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="none" d="M0 0h64v64H0z" data-name="Viewbox 64x64"/><g fill="#00962d"><path d="M53.48 7.54H10.53c-3.59 0-6.52 2.94-6.52 6.52v25.45c0 3.59 2.94 6.52 6.52 6.52h8.65l.03 10.5c6.46-.02 11.93-4.5 13.5-10.5h20.78c3.59 0 6.52-2.94 6.52-6.52V14.06c0-3.59-2.94-6.52-6.52-6.52Zm3.02 31.98c0 1.67-1.36 3.02-3.02 3.02H29.67c0 4.55-2.92 8.44-6.98 9.89l-.02-9.89H10.53c-1.67 0-3.02-1.36-3.02-3.02V14.06c0-1.67 1.36-3.02 3.02-3.02h42.95c1.67 0 3.02 1.36 3.02 3.02v25.45Z"/><path d="M37.35 29.74 46 23.45H35.3L32 13.26l-3.3 10.19H18l8.65 6.29-3.3 10.19 8.65-6.3 8.65 6.3-3.3-10.19z"/></g></svg>
\ No newline at end of file
<svg data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="none" d="M0 0h64v64H0z" data-name="Viewbox 64x64"/><g fill="#6e6e6e"><path d="M53.48 7.54H10.53c-3.59 0-6.52 2.94-6.52 6.52v25.45c0 3.59 2.94 6.52 6.52 6.52h8.65l.03 10.5c6.46-.02 11.93-4.5 13.5-10.5h20.78c3.59 0 6.52-2.94 6.52-6.52V14.06c0-3.59-2.94-6.52-6.52-6.52Zm3.02 31.98c0 1.67-1.36 3.02-3.02 3.02H29.67c0 4.55-2.92 8.44-6.98 9.89l-.02-9.89H10.53c-1.67 0-3.02-1.36-3.02-3.02V14.06c0-1.67 1.36-3.02 3.02-3.02h42.95c1.67 0 3.02 1.36 3.02 3.02v25.45Z"/><path d="M37.35 29.74 46 23.45H35.3L32 13.26l-3.3 10.19H18l8.65 6.29-3.3 10.19 8.65-6.3 8.65 6.3-3.3-10.19z"/></g></svg>
\ No newline at end of file
<svg data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="none" d="M0 0h64v64H0z" data-name="Viewbox 64x64"/><g fill="#cb1800"><path d="M53.48 7.54H10.53c-3.59 0-6.52 2.94-6.52 6.52v25.45c0 3.59 2.94 6.52 6.52 6.52h8.65l.03 10.5c6.46-.02 11.93-4.5 13.5-10.5h20.78c3.59 0 6.52-2.94 6.52-6.52V14.06c0-3.59-2.94-6.52-6.52-6.52Zm3.02 31.98c0 1.67-1.36 3.02-3.02 3.02H29.67c0 4.55-2.92 8.44-6.98 9.89l-.02-9.89H10.53c-1.67 0-3.02-1.36-3.02-3.02V14.06c0-1.67 1.36-3.02 3.02-3.02h42.95c1.67 0 3.02 1.36 3.02 3.02v25.45Z"/><path d="M37.35 29.74 46 23.45H35.3L32 13.26l-3.3 10.19H18l8.65 6.29-3.3 10.19 8.65-6.3 8.65 6.3-3.3-10.19z"/></g></svg>
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment