This repository has been archived on 2024-10-25. You can view files and clone it, but cannot push or open issues or pull requests.
Sequenzia/app/models/History.php
2013-10-26 18:06:58 -05:00

225 lines
8.2 KiB
PHP
Executable File

<?php
class History extends Rails\ActiveRecord\Base
{
public function aux()
{
if (!$this->aux_as_json) {
return [];
}
return json_decode($this->aux_as_json);
}
public function setAux($o)
{
if (!$o) {
$this->aux_as_json = null;
} else {
$this->aux_as_json = json_encode($o);
}
}
public function group_by_table_class()
{
return Rails::services()->get('inflector')->classify($this->group_by_table);
}
public function get_group_by_controller()
{
$class_name = $this->group_by_table_class();
return $class_name::versioning()->get_versioning_group_by()['controller'];
}
public function get_group_by_action()
{
$class_name = $this->group_by_table_class();
return $class_name::versioning()->get_versioning_group_by()['action'];
}
public function group_by_obj()
{
$class_name = $this->group_by_table_class();
return $class_name::find($this->group_by_id);
}
public function user()
{
return User::find($this->user_id);
}
public function author()
{
return User::find($this->user_id)->name;
}
# Undo all changes in the array changes.
static public function undo($changes, $user, $redo_change = false, array &$errors)
{
# Save parent objects after child objects, so changes to the children are
# committed when we save the parents.
$objects = new ArrayObject();
foreach ($changes as $change) {
// # MI: If we're redoing, why would we need a previous?
// # Adding this conditional so redos won't need a previous.
// if (!$redo_change) {
# If we have no previous change, this was the first change to this property
# and we have no default, so this change can't be undone.
$previous_change = $change->previous;
if (!$previous_change && empty($change->options()['allow_reverting_to_default'])) {
continue;
}
// }
if (!$user->can_change($change->obj(), $change->column_name)) {
$errors[spl_object_hash($change)] = 'denied';
continue;
}
# Add this node and its parent objects to objects.
$node = self::cache_object_recurse($objects, $change->table_name, $change->remote_id, $change->obj());
if (!isset($node['changes'])) {
$node['changes'] = [];
}
$node['changes'][] = $change;
}
if (empty($objects['objects'])) {
return;
}
# objects contains one or more trees of objects. Flatten this to an ordered
# list, so we can always save child nodes before parent nodes.
$done = new ArrayObject();
$stack = new ArrayObject();
foreach ($objects['objects'] as $table_name => $rhs) {
foreach ($rhs as $id => $node) {
# Start adding from the node at the top of the tree.
while (!empty($node['parent'])) {
$node = $node['parent'];
}
self::stack_object_recurse($node, $stack, $done);
}
}
$stack = $stack->getArrayCopy();
foreach (array_reverse($stack) as $node) {
$object = $node['o'];
/**
* MI: Only Pool model sets the undo (:after) callback.
* Calling it manually at the end because runCallbacks doesn't behave like in Rails.
* TODO: fix callbacks in the framework and update this.
*/
// $object->runCallbacks('undo', function() {
$changes = !empty($node['changes']) ? $node['changes'] : [];
if ($changes) {
foreach ($changes as $change) {
if ($redo_change) {
$redo_func = $change->column_name . '_redo';
if (method_exists($object, $redo_func)) {
$object->$redo_func($change);
} else {
$object->{$change->column_name} = $change->value;
}
} else {
$undo_func = $change->column_name . '_undo';
if (method_exists($object, $undo_func)) {
$object->$undo_func($change);
} else {
if ($change->previous) {
$previous = $change->previous->value;
} else {
$previous = $change->options()['default']; # when :allow_reverting_to_default
}
$object->{$change->column_name} = $previous;
}
}
}
}
$object->runCallbacks('after_undo');
// });
$object->save();
}
}
static public function generate_sql($options = [])
{
$sql = self::none();
if (isset($options['remote_id']))
$sql->where("histories.remote_id = ?", $options['remote_id']);
if (isset($options['remote_id']))
$sql->where("histories.user_id = ?", $options['user_id']);
if (isset($options['user_name'])) {
$sql->joins("JOIN users ON users.id = histories.user_id")
->where("users.name = ?", $options['user_name']);
}
return $sql;
}
# Find and return the node for table_name/id in objects. If the node doesn't
# exist, create it and point it at object.
static protected function cache_object($objects, $table_name, $id, $object)
{
if (empty($objects['objects']))
$objects['objects'] = [];
if (empty($objects['objects'][$table_name]))
$objects['objects'][$table_name] = [];
if (empty($objects['objects'][$table_name][$id]))
$objects['objects'][$table_name][$id] = new ArrayObject([
'o' => $object
]);
return $objects['objects'][$table_name][$id];
}
# Find and return the node for table_name/id in objects. Recursively create
# nodes for parent objects.
static protected function cache_object_recurse($objects, $table_name, $id, $object)
{
$node = self::cache_object($objects, $table_name, $id, $object);
# If this class has a master class, register the master object for update callbacks too.
$master = $object->versioned_master_object();
if ($master) {
$master_node = self::cache_object_recurse($objects, get_class($master), $master->id, $master);
if (empty($master_node['children']))
$master_node['children'] = [];
$master_node['children'][] = $node;
$node['parent'] = $master_node;
}
return $node;
}
static protected function stack_object_recurse($node, $stack, $done = [])
{
$nodeHash = spl_object_hash($node);
if (!empty($done[$nodeHash]))
return;
$done[$nodeHash] = true;
$stack[] = $node;
if (!empty($node['children'])) {
foreach ($node['children'] as $child) {
self::stack_object_recurse($child, $stack, $done);
}
}
}
protected function associations()
{
return [
'belongs_to' => [
'user'
],
'has_many' => [
'history_changes' => [function() { $this->order('id'); }]
]
];
}
}