Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
# Changelog
All notable changes to this project will be documented in this file.

## 0.11.2
## Added
- Added patch exception, when entity_type does not have attribute
- Added exceptions that contain HTTP status codes for api handling
## Fixes
- Fixed error when updating array based attribute value (AttributeValueId was removed when list was empty and not recreated when list contained values again)
### Changed
- Moved patch logic from EntityController to Models
- Moved epoch attribute testing from into epoch attribute test
- Patch attribute endpoint: moved aid param from params obj to top level, as other values were not used

## 0.11.1
### Added
- Unit Tests for Directory.php
### Changed
- Added path value to XSRF cookie to prevent overwriting by another instance at the same domain
- _Show x replies_ button in comment list moved to header
- XSRF Token get's a custom name depending on the deployed app-name. Allows for deploying multiple instances on the same domain at different paths.
### Fixed
- Login not possible on instances in subfolder
- Last editor not visible in _Entity Detail_ tab
Expand Down
88 changes: 88 additions & 0 deletions app/AttributeValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

use App\Geodata;
use App\AttributeTypes\AttributeBase;
use App\Exceptions\InvalidDataException;
use App\Exceptions\Status\UnprocessableContentException;
use App\Traits\CommentTrait;
use App\Traits\ModerationTrait;
use Clickbar\Magellan\Data\Geometries\Geometry;
use Illuminate\Database\Eloquent\Model;
use Exception;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
use Spatie\Searchable\Searchable;
Expand Down Expand Up @@ -54,6 +57,10 @@ class AttributeValue extends Model implements Searchable
'thesaurus_val'
];

protected $appends = [
'value'
];

protected $casts = [
'geography_val' => Geometry::class,
];
Expand Down Expand Up @@ -87,6 +94,10 @@ public function getValue() {
return AttributeBase::serializeValue($this);
}

public function getValueAttribute() {
return $this->getValue();
}

public static function getValueFromKey($arr) {
if(!isset($arr)) return null;

Expand Down Expand Up @@ -131,9 +142,86 @@ public function patch($values) {
}
$this->save();
}

public static function remove($entityId, $attributeId): ?AttributeValue {
try{
$attrval = AttributeValue::where([
['entity_id', '=', $entityId],
['attribute_id', '=', $attributeId],
])->firstOrFail();

// If the user is moderated, he cannot delete the value directly
if(auth()->user()->isModerated()) {
$attrval->moderate('pending-delete', true);
} else {
$attrval->delete();
}
return $attrval;
}catch(ModelNotFoundException $e){
throw new Exception(__('This attribute value does either not exist or is in moderation state.'));
}
}

public static function upsert($entityId, $attributeId, $value): ?AttributeValue{
if(!isset($entityId) || !isset($attributeId)) {
throw new \InvalidArgumentException('Entity ID and Attribute ID must be provided.');
}

try{
$attribute = Attribute::findOrFail($attributeId);
} catch(ModelNotFoundException $e) {
throw new UnprocessableContentException(__('Attribute does not exist.'));
}

try{
$formKeyValue = AttributeValue::getFormattedKeyValue($attribute->datatype, $value);
}catch(InvalidDataException $e) {
throw new UnprocessableContentException($e->getMessage());
}

// Check if entity_type does even have the attribute
$entity = Entity::find($entityId);
if(!$entity->entity_type->hasEntityAttribute($attributeId)) {
throw new UnprocessableContentException(__('Attribute is not part of the entity type of this entity.'));
}

$alreadyModerated = AttributeValue::where('entity_id', $entityId)
->where('attribute_id', $attributeId)
->onlyModerated()
->exists();

$user = auth()->user();
// Currently the logic is that a moderated state cannot be changed
// by a moderated user.
if($alreadyModerated && $user->isModerated()) {
throw new Exception(__('This attribute value is in moderation state. A user with appropriate permissions has to accept or deny it first.'));
}
$attributeValue = AttributeValue::firstOrNew([
'entity_id' => $entityId,
'attribute_id' => $attributeId,
], [
'certainty' => null
]);

$attributeValue->entity_id = $entityId;
$attributeValue->attribute_id = $attributeId;
$attributeValue->{$formKeyValue->key} = $formKeyValue->val;
$attributeValue->user_id = $user->id;
$attributeValue->save();

if($user->isModerated()) {
$attributeValue = $attributeValue->moderate('pending', false, true);
unset($attributeValue->comments_count);
}

return $attributeValue;
}

public static function getFormattedKeyValue($datatype, $rawValue) : stdClass {
$class = AttributeBase::getMatchingClass($datatype);
if($class == false) {
throw new InvalidDataException("Attribute of type '$datatype' is not supported. Maybe the underlying plugin is disabled or missing! You may still save all other properties.");
}
$keyValue = new stdClass();
$keyValue->key = $class::getField();
$keyValue->val = $class::unserialize($rawValue);
Expand Down
64 changes: 64 additions & 0 deletions app/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\AttributeTypes\AttributeBase;
use App\AttributeTypes\SqlAttribute;
use App\Exceptions\AmbiguousValueException;
use App\Exceptions\Status\UnprocessableContentException;
use App\Import\EntityImporter;
use App\Traits\CommentTrait;

Expand Down Expand Up @@ -319,6 +320,69 @@ public static function patchRanks($rank, $id, $parent, $user) {
DB::commit();
}

public function patchAttributes($patchedAttributes, $user)
{
$addedAttributes = [];
$removedAttributes = [];
$changedAttributes = [];

DB::beginTransaction();
foreach($patchedAttributes as $patch) {
$aid = $patch['aid'];
$value = $patch['value'];
$operation = $patch['op'];

try{
if($operation == 'remove'){
$attributeValue = AttributeValue::remove($this->id, $aid);
if(!$user->isModerated()){
$removedAttributes[$aid] = $attributeValue;
}
}else{
/**
* In the case when a user created the attribute, while another was visiting the
* page and sends an 'add' operation, and the other user also sends his changes,
* the application would have thrown an error, that the attribute was already created.
*
* That's why we combined the add and replace operations into one case.
* [SO] 29.01.2025
*/
$attributeValue = AttributeValue::upsert($this->id, $aid, $value);
if($attributeValue->wasRecentlyCreated) {
$addedAttributes[$aid] = $attributeValue;
} else {
$changedAttributes[$aid] = $attributeValue;
}
}
} catch(UnprocessableContentException $e) {
DB::rollBack();
throw $e;
} catch(Exception $e){
DB::rollBack();
throw $e;
}
}

// Save model if last editor changed
// Only update timestamps otherwise
$this->user_id = $user->id;
if($this->isDirty()) {
$this->save();
}else{
$this->touch();
}

DB::commit();
$this->load('user');

return [
'entity' => $this,
'added_attributes' => $addedAttributes,
'changed_attributes' => $changedAttributes,
'removed_attributes' => $removedAttributes,
];
}

public function child_entities() {
return $this->hasMany('App\Entity', 'root_entity_id')->orderBy('id');
}
Expand Down
14 changes: 14 additions & 0 deletions app/EntityType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App;

use App\EntityAttribute;
use App\EntityAttributePivot;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Traits\LogsActivity;
Expand Down Expand Up @@ -77,4 +78,17 @@ public function sub_entity_types() {
public function thesaurus_concept() {
return $this->belongsTo('App\ThConcept', 'thesaurus_url', 'concept_url');
}

/**
* Check if this entity type has the given attribute assigned
*
* Note: HasAttribute would have been a better name, but it collides with
* Laravel's internal method of the same name.
*/
public function hasEntityAttribute($attributeId) {
return EntityAttribute::where('entity_type_id', $this->id)
->where('attribute_id', $attributeId)
->exists();
}

}
19 changes: 19 additions & 0 deletions app/Exceptions/Status/MalformedContentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Exceptions\Status;

use Exception;


/**
* This Exception is thrown when the provided content is syntactically erroneous.
* For example, when the JSON is not well-formed.
* It corresponds to HTTP status code 400 Bad Request.
*/
class MalformedContentException extends StatusCodeException
{
public function getStatusCode(): int
{
return 400;
}
}
15 changes: 15 additions & 0 deletions app/Exceptions/Status/StatusCodeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace App\Exceptions\Status;

use Exception;


/**
* Base class for exceptions that contain an HTTP status code.
* and can be used to determine the appropriate HTTP response code.
*/
abstract class StatusCodeException extends Exception
{
abstract public function getStatusCode(): int;
}
16 changes: 16 additions & 0 deletions app/Exceptions/Status/UnprocessableContentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Exceptions\Status;

/**
* This Exception is thrown when the provided content is syntactically correct but semantically erroneous.
* For example, when trying to set an attribute value that does not conform to the attribute's type.
* It corresponds to HTTP status code 422 Unprocessable Entity.
*/
class UnprocessableContentException extends StatusCodeException
{
public function getStatusCode(): int
{
return 422;
}
}
Loading
Loading