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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
# 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
- **BACKPORT::0.12** API endpoint for getting all entity details data in one request: GET::v1/entity/{id}/entity_detail
### Changed
- Alerts can now be dismissed
- 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
- **BACKPORT::0.12** Entity metadata is only loaded when accessing the metadata tab

### Fixed
- Fixed error when updating array based attribute value (AttributeValueId was removed when list was empty and not recreated when list contained values again)
- **BACKPORT::0.12** Removed redundant calls to the entity endpoint
- **BACKPORT::0.12** Metadata tab error on submit (unknown variable)

## 0.11.1
### Added
- Unit Tests for Directory.php
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
68 changes: 68 additions & 0 deletions app/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace App;

use Exception;

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 +322,71 @@ 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();

// TODO: Is this necessary?
$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