package GCData; ################################################### # # Copyright 2005-2010 Christian Jodar # # This file is part of GCstar. # # GCstar is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # GCstar is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with GCstar; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA # ################################################### use strict; { package GCItems; # # This is seen as $main->{items} # use XML::Parser; use Storable; use File::Copy; use File::Path; use File::Basename; use GCModel; use GCBackend::GCBackendXmlParser; sub new { my ($proto, $parent) = @_; my $class = ref($proto) || $proto; my $self = {}; $self->{parent} = $parent; $self->{imagesToBeRemoved} = []; $self->{imagesToBeAdded} = []; $self->{loaded} = {}; $self->{currentItem} = -1; $self->{hasBeenDeleted} = 0; $self->{block} = 0; $self->{previousFile} = 0; #$self->{filterSearch} = new GCFilterSearch; bless ($self, $class); return $self; } sub initModel { #if $modelChanged is set, that means we updated the currently used model # and not that we changed the model my ($self, $model, $modelUpdated) = @_; $self->{model} = $model; $self->{parent}->notifyModelChange($modelUpdated); } sub setPanel { my ($self, $panel) = @_; $self->{panel} = $panel; } sub unselect { my ($self) = @_; $self->{currentItem} = -1; } sub updateSelectedItemInfoFromGivenPanel { my ($self, $panel) = @_; my $previousPanel = $self->{panel}; $self->{panel} = $panel; $self->updateSelectedItemInfoFromPanel(1); $self->{panel} = $previousPanel; } sub getInfoFromPanel { # $info contains default values that will be merged with new ones my ($self, $panel, $info) = @_; my $idField = $self->{model}->{commonFields}->{id}; my $panelId = $panel->$idField; my $previousId = $info->{$idField}; my $changed = 0; if ($panelId && ($panelId != $previousId)) { $info->{$idField} = $panel->$idField; $self->{loaded}->{information}->{maxId} = $panelId if ($panelId > $self->{loaded}->{information}->{maxId}); $self->findMaxId if $previousId == $self->{loaded}->{information}->{maxId}; $changed = 1; } $panel->$idField($info->{$idField}) if $panel->$idField; my $previous = {$idField => $previousId}; for my $field (@{$self->{model}->{fieldsNames}}) { next if $field eq $idField; $previous->{$field} = $info->{$field}; next if !$panel->{$field}->hasChanged; $panel->{$field}->addHistory if ($self->{model}->{fieldsInfo}->{$field}->{hasHistory}); $changed = 1; $info->{$field} = $panel->$field; $self->{parent}->{menubar}->checkFilter($field); } return ($changed, $info, $previous); } sub updateSelectedItemInfoFromPanel { my ($self, $withSelect, $forced) = @_; my $selectedChanged = 0; my $filtered = 0; return $selectedChanged if $self->{currentItem} == -1; my $info; if ($self->{multipleMode}) { $info = {}; } else { $info = ($self->getItemsListFiltered)->[$self->{currentItem}]; } my $changed; my $previous; ($changed, $info, $previous) = $self->getInfoFromPanel($self->{panel}, $info); if ($forced) { $previous->{$_} = 'GCS_FORCED' foreach @$forced; } if ($changed) { if ($self->{multipleMode}) { my $newIdx; # Propagate the changes to all the items foreach (@{$self->{multipleCurrentItems}}) { my $previous = Storable::dclone(($self->getItemsListFiltered)->[$_]); my $item = ($self->getItemsListFiltered)->[$_]; for my $field (keys %$info) { $item->{$field} = $info->{$field}; } $self->{panel}->dataChanged($item, 1); $newIdx = $self->{parent}->{itemsView}->changeItem($_, $previous, $item); if ($newIdx != $_) { $selectedChanged = 1; } } if ($selectedChanged) { $self->{currentItem} = $self->{parent}->{itemsView}->getCurrentIdx; $self->{multipleMode} = 0; $self->displayCurrent; } } else { $self->{panel}->dataChanged($info); my $current = $self->{parent}->{itemsView}->changeCurrent($previous, $info, $self->{currentItem}, $withSelect); # If we didn't selected the same, the selection didn't change if ($current != $self->{currentItem}) { $self->{currentItem} = $current; $self->displayCurrent if $withSelect; $selectedChanged = 1; } if ($selectedChanged) { $filtered = 1; $selectedChanged = 0 if !$withSelect; } } $self->{parent}->checkPanelVisibility; $self->{parent}->markAsUpdated; } return ($selectedChanged, $filtered); } sub getTitle { my ($self, $idx) = @_; if ($self->{multipleMode}) { # Multiple items selected, so just use collection title or filename my $name; if ($self->{parent}->{options}->file) { $name = $self->{parent}->{items}->getInformation->{name} if $self->{parent}->{items}; $name ||= basename($self->{parent}->{options}->file); } else { $name = $self->{parent}->{lang}->{UnsavedCollection}; } return $name; } else { my $realIdx = $idx; $realIdx = $self->{currentItem} if ! defined $idx; return ($self->getItemsListFiltered)->[$realIdx]->{$self->{model}->{commonFields}->{title}}; } } sub getCurrent { my ($self) = @_; return $self->{currentItem}; } sub displayCurrent { my ($self) = @_; $self->displayInPanel($self->{panel}, undef); } sub displayInPanel { my ($self, $panel, $idx) = @_; my $info; if ($self->{multipleMode}) { # We merge all the items here my %fields = map {$_ => 1} @{$self->{model}->{fieldsNotFormatted}}; foreach (@{$self->{multipleCurrentItems}}) { my $item = ($self->getItemsListFiltered)->[$_]; for my $field (keys %fields) { if (exists $info->{$field}) { if ($self->transformValue($info->{$field}, $field) ne $self->transformValue($item->{$field}, $field)) { $info->{$field} = ''; delete $fields{$field}; # TODO store the information also elsewhere to mark # the fields in panel. Or mark it immediately with something # such as # panel->markAsDirty($field); } } else { $info->{$field} = $item->{$field}; } } } } else { $idx = $self->{currentItem} if ! defined $idx; return if $self->{currentItem} < 0; $info = ($self->getItemsListFiltered)->[$idx]; } $self->displayDataInPanel($panel, $info); } sub displayDataInPanel { my ($self, $panel, $info) = @_; for my $field (@{$self->{model}->{fieldsNotFormatted}}) { $panel->$field($info->{$field}); $panel->{$field}->resetChanged if $panel->{$field}; } $panel->dataChanged; $GCGraphicComponent::somethingChanged = 0; } sub display { my $self = shift; return if (! $self->{itemArray}) || (! scalar @{$self->{itemArray}}); my @numbers = @_; my $number; my $multipleMode; my $withSelect = 0; my $noUpdate = 0; if ($#numbers > 0) { $multipleMode = 1; $self->{multipleCurrentItems} = \@numbers; } else { $multipleMode = 0; $number = $numbers[0]; if ($number == -1) { $number = 0; $noUpdate = 1; } # We want a selection if user clicked on the same one $withSelect = ($number == $self->{currentItem}); } my ($selectedHasChanged, $filtered) = (0, 0); if ((!$noUpdate) && ($self->{currentItem} > -1) && !($self->{hasBeenDeleted})) { ($selectedHasChanged, $filtered) = $self->updateSelectedItemInfoFromPanel($withSelect); } else { $self->{currentItem} = $number if !$multipleMode; } $self->{multipleMode} = $multipleMode; $self->{hasBeenDeleted} = 0; $self->{currentItem} = $number if !$selectedHasChanged && !$multipleMode; $self->displayCurrent if !$selectedHasChanged; return ($selectedHasChanged || $filtered); } sub valueToDisplayed { my ($self, $value, $field) = @_; my $displayed = $self->{model}->getDisplayedValue($self->{model}->{fieldsInfo}->{$field}->{values}, $value); return $displayed if $displayed; # For personal models, it won't return a value. Then we keep the original one. return $value; } sub transformValue { my ($self, $value, $field, $type) = @_; $type ||= $self->{model}->{fieldsInfo}->{$field}->{type}; $value = $self->{parent}->transformTitle($value) if $field eq $self->{model}->{commonFields}->{title}; #$value = GCPreProcess::reverseDate($value) if $type eq 'date'; $value = GCUtils::timeToStr($value, $self->{parent}->{options}->dateFormat) if $type eq 'date'; $value = $self->valueToDisplayed($value, $field) if $type eq 'options'; $value = GCPreProcess::multipleList($value, $type) if $type =~ /list$/o; return $value; } sub getValue { my ($self, $idx, $field) = @_; return ($self->getItemsListFiltered)->[$idx]->{$field}; } sub setValue { my ($self, $idx, $field, $value) = @_; ($self->getItemsListFiltered)->[$idx]->{$field} = $value; if ($idx == $self->{currentItem}) { $self->{panel}->$field($value); $self->{panel}->dataChanged; } } sub getItemsListFiltered { my ($self, $filter) = @_; return $self->{itemArray} if ! $filter; my @results = (); foreach (@{$self->{itemArray}}) { if ($self->{parent}->{filterSearch}->test($_)) { push @results, $_; } } return \@results; } # Should only be used by GCCommandExecution sub setItemsList { my ($self, $itemsList) = @_; $self->{itemArray} = $itemsList; } sub getInformation { my $self = shift; $self->{loaded}->{information} ||= {}; return $self->{loaded}->{information}; } sub setInformation { my ($self, $info) = @_; $self->{loaded}->{information} = $info; } sub reloadList { my ($self, $splash, $fullProgress, $filtering) = @_; return if $self->{block}; if ($splash) { $splash->initProgress if $fullProgress; $splash->setItemsTotal(scalar @{$self->{itemArray}}) if $self->{itemArray}; } $self->{parent}->{itemsView}->reset if $self->{parent}->{itemsView}; my $lastDisplayed = -1; my $hasId = 0; my $j = 0; my $idField = $self->{model}->{commonFields}->{id}; my $currentId; # If we don't get an history from BE, we will have to initialize it now my $historyNeeded = ! $self->{loaded}->{gotHistory}; my %histories; foreach (@{$self->{itemArray}}) { if ($historyNeeded) { foreach my $field (@{$self->{model}->{fieldsHistory}}) { #push @{$histories{$field}}, $_->{$field}; $self->{panel}->addHistory($_->{$field}, 1); } } $currentId = $_->{$idField}; $self->{parent}->{itemsView}->addItem($_, 0); $lastDisplayed = $j; $splash->setProgressForItemsDisplay($j) if $splash; $j++; } if ($historyNeeded) { for my $hfield(@{$self->{model}->{fieldsHistory}}) { $self->{panel}->{$hfield}->setDropDown; } # Now we are sure we got one $self->{loaded}->{gotHistory} = 2; } $self->{panel}->show if $j; #if ($splash && $fullProgress) #{ # $splash->endProgress; #} if (! $self->{parent}->{initializing}) { $self->{parent}->reloadDone(0, $splash); $self->select($self->{currentItem}, 0); } } sub select { my ($self, $value, $init) = @_; return if !$self->{parent}->{itemsView}; return $self->{parent}->{itemsView}->select($value, $init) unless $value < -1; } sub removeCurrentItems { my $self = shift; my $numbers = $self->{parent}->{itemsView}->getCurrentItems; my $nbRemoved = 0; # Numerically sort list foreach my $number(sort {$a <=> $b} @$numbers) { # We need to adjust it because we already removed other ones. my $actualNumber = $number - $nbRemoved; foreach (@{$self->{model}->{managedImages}}) { my $image = $self->{itemArray}->[$actualNumber]->{$_}; $self->{parent}->checkPictureToBeRemoved($image); } splice @{$self->{itemArray}}, $actualNumber, 1; $nbRemoved++; } my $newIdx = $self->{parent}->{itemsView}->removeCurrentItems; #($number); $self->{currentItem} = $newIdx; $self->{multipleMode} = 0; $self->{hasBeenDeleted} = 1; $self->displayCurrent; } sub addItem { my ($self, $info, $keepId, $noSelect) = @_; my $nbItems = scalar @{$self->{itemArray}}; # $self->{panel}->show if ! $nbItems; my $currentId; if ($keepId) { $currentId = $self->{itemArray}->[$nbItems]->{$self->{model}->{commonFields}->{id}}; } else { $self->{loaded}->{information}->{maxId}++; $currentId = $self->{loaded}->{information}->{maxId}; $self->{itemArray}->[$nbItems]->{$self->{model}->{commonFields}->{id}} = $currentId; } for my $field (@{$self->{model}->{fieldsNames}}) { next if $field eq $self->{model}->{commonFields}->{id}; if ($self->{model}->{fieldsInfo}->{$field}->{hasHistory}) { $self->{panel}->{$field}->addHistory($info->{$field}); } $self->{itemArray}->[$nbItems]->{$field} = $info->{$field}; } $self->{parent}->{itemsView}->addItem($self->{itemArray}->[$nbItems], 1); $self->{multipleMode} = 0; $self->{currentItem} = $nbItems; $self->displayCurrent; $self->select($nbItems, 0) if !$noSelect; $self->{parent}->{itemsView}->showCurrent; return $currentId; } sub setOptions { my ($self, $options) = @_; $self->{options} = $options; #return $self->load($options->file, $splash, 0); } sub markToBeRemoved { my ($self, $image) = @_; push @{$self->{imagesToBeRemoved}}, $image; } sub markToBeAdded { my ($self, $image) = @_; push @{$self->{imagesToBeAdded}}, $image; } sub removeMarkedPictures { my $self = shift; my $image; foreach $image(@{$self->{imagesToBeRemoved}}) { unlink $image; } $self->{imagesToBeRemoved} = []; } sub addMarkedPictures { my $self = shift; $self->{imagesToBeAdded} = []; } sub clean { my $self = shift; my $image; foreach (@{$self->{imagesToBeAdded}}) { unlink $_; } $self->{oldImagesDirectory} = {}; $self->{newImagesDirectory} = undef; $self->{copyImagesWhenChangingDir} = 0; } sub setNewImagesDirectory { # $prev is also a parameter because we didn't store it here my ($self, $new, $prev, $withCopy) = @_; $new =~ s|/$||; $self->{newImagesDirectory} = $new; $self->{copyImagesWhenChangingDir} = $withCopy; # We stored the previous one as a hash so it will be easier for tests $prev =~ s|/$||; $self->{oldImagesDirectory}->{$prev} = 1; } sub setPreviousFile { my ($self, $prev) = @_; $self->{previousFile} = $prev; } sub queryReplace { my ($self, $field, $old, $new, $caseSensitive) = @_; foreach (@{$self->{itemArray}}) { if (ref($_->{$field}) eq 'ARRAY') { foreach my $subval(@{$_->{$field}}) { foreach my $val(@$subval) { if ($caseSensitive) { $val =~ s/$old/$new/g; } else { $val =~ s/$old/$new/gi; } } } } else { if ($caseSensitive) { $_->{$field} =~ s/$old/$new/g; } else { $_->{$field} =~ s/$old/$new/gi; } } } $self->displayCurrent; $self->reloadList; } sub findMaxId { my $self = shift; $self->{loaded}->{information}->{maxId} = -1; foreach (@{$self->{itemArray}}) { $self->{loaded}->{information}->{maxId} = $_->{$self->{model}->{commonFields}->{id}} if $_->{$self->{model}->{commonFields}->{id}} > $self->{loaded}->{information}->{maxId}; } } sub clearList { my $self = shift; $self->{currentItem} = -1; $self->{loaded} = {}; $self->{itemArray} = []; $self->{panel}->hide if $self->{panel}; $self->{parent}->{itemsView}->clearCache if $self->{parent}->{itemsView}; $self->{parent}->{itemsView}->reset if $self->{parent}->{itemsView}; #$self->{parent}->reloadDone(1) if ! $self->{parent}->{initializing}; #$self->reloadList if ! $self->{parent}->{initializing}; } sub getNbItems { my $self = shift; return 0 if ! $self->{itemArray}; return scalar @{$self->{itemArray}}; } sub setLock { my ($self, $value) = @_; $self->{loaded}->{information}->{locked} = $value; } sub getLock { my $self = shift; return $self->{loaded}->{information}->{locked}; } sub getBackend { my ($self, $file) = @_; $self->{backend} = new GCBackend::GCBeXmlParser($self->{parent}) if !$self->{backend}; $self->{backend}->setParameters(file => $file, version => $self->{parent}->{version}); return $self->{backend}; } sub getVersion { my ($self, $file) = @_; return $self->getBackend($file)->getVersion; } sub load { my ($self, $file, $splash, $fullProgress, $noReload) = @_; my $initTime; if ($ENV{GCS_PROFILING} > 0) { eval 'use Time::HiRes'; eval '$initTime = [Time::HiRes::gettimeofday()]'; } $self->clean; if (!$file) { $self->{parent}->setCurrentModel; return 0; } my $collection; $self->{block} = 1; $self->clearList; $self->{block} = 0; $self->{splash} = $splash; my $backend; eval { $backend = $self->getBackend($file); $self->{loaded} = $backend->load($splash); # We keep a direct access to this one $self->{itemArray} = $self->{loaded}->{data}; }; if ($@) { my @error = ('Fatal error while reading file', $@); return (0, \@error); } elsif ($self->{loaded}->{error}) { return (0, $self->{loaded}->{error}); } # Perform Models Change if needed if(!$self->{model}->{isInline} && ($backend->getVersion() ne $backend->{version})) { my $modelFormatUpdater=GCModelsChanges->new($self,$self->{model}->{collection}->{name}); $modelFormatUpdater->applyChanges($self->{itemArray}, $backend->getVersion(), $backend->{version}); } # Hide the panel if no item if (! scalar @{$self->{itemArray}}) { $self->{panel}->hide; } # gotHistory = 1 means we got one but it has not been set in components if ($self->{loaded}->{gotHistory} == 1) { for my $hfield(@{$self->{model}->{fieldsHistory}}) { if (exists $self->{loaded}->{histories}->{$hfield}) { $self->{panel}->{$hfield}->setValues($self->{loaded}->{histories}->{$hfield}, 1); } $self->{panel}->{$hfield}->setDropDown; } # $self->{loaded}->{gotHistory} = 2; } elsif ($self->{loaded}->{gotHistory} == 2) { for my $hfield(@{$self->{model}->{fieldsHistory}}) { $self->{panel}->{$hfield}->setDropDown; } } $self->reloadList($splash, $fullProgress) unless $noReload; if ($ENV{GCS_PROFILING} > 0) { my $elapsed; eval '$elapsed = Time::HiRes::tv_interval($initTime)'; print "Load time : $elapsed\n"; } return 1; } sub movePictures { my ($self) = @_; eval { mkpath $self->{newImagesDirectory}; my $file; my $dataFile = $self->{previousFile} ? $self->{previousFile} : $self->{options}->file; foreach (@{$self->getItemsListFiltered}) { foreach my $pic(@{$self->{model}->{fieldsImage}}) { $file = GCUtils::getDisplayedImage($_->{$pic}, '', $dataFile); # Not moving picture if it is not in the previous directory next if !$file; next if ! exists $self->{oldImagesDirectory}->{Cwd::realpath(dirname($file))}; (my $suffix = $file) =~ s/.*?(\.[^.]*)$/$1/; my $new = $self->{parent}->getUniqueImageFileName( $suffix, $_->{$self->{model}->{commonFields}->{title}}); if ($self->{copyImagesWhenChangingDir}) { copy $file, $new; } else { move $file, $new; } $_->{$pic} = $new; } } }; $self->{previousFile} = 0; return $@ if $@; $self->displayCurrent; $self->{newImagesDirectory} = undef; $self->{copyImagesWhenChangingDir} = 0; $self->{oldImagesDirectory} = {}; return 0; } sub save { my ($self, $splash) = @_; my $initTime; if ($ENV{GCS_PROFILING} > 0) { eval 'use Time::HiRes'; eval '$initTime = [Time::HiRes::gettimeofday()]'; } $self->updateSelectedItemInfoFromPanel if ($self->{currentItem} > -1); # TODO : Use progress bar for this operation also my $moveError = $self->movePictures if $self->{newImagesDirectory}; return (0, ['SaveError', $moveError]) if $moveError; if ($splash) { $splash->initProgress($self->{parent}->{lang}->{StatusSave}); $splash->setItemsTotal(scalar @{$self->{itemArray}}); } $self->addMarkedPictures; $self->removeMarkedPictures; my $backend = $self->getBackend($self->{options}->file); # We re-generate histories to give it to backend # Deactivated for the moment # if ($self->{panel}) # { # my %histories; # for my $hfield(@{$self->{model}->{fieldsHistory}}) # { # $histories{$hfield} = $self->{panel}->{$hfield}->getValues; # } # $backend->setHistories(\%histories); # } my $result = $backend->save($self->{itemArray}, $self->{loaded}->{information}, $splash); $self->{parent}->endProgress; if ($result->{error}) { return (0, $result->{error}); } $self->{parent}->removeUpdatedMark; if ($ENV{GCS_PROFILING} > 0) { my $elapsed; eval '$elapsed = Time::HiRes::tv_interval($initTime)'; print "Save time : $elapsed\n"; } return 1; } sub getSummary { my ($self, $idx) = @_; my $info = ($self->getItemsListFiltered)->[$idx]; my $summary = "".GCUtils::encodeEntities($info->{$self->{model}->{commonFields}->{title}})."\n"; for my $field (@{$self->{model}->getSummaryFields}) { my $value = $info->{$field}; if ($field eq $self->{model}->{commonFields}->{borrower}->{name}) { $value = $self->{parent}->{lang}->{PanelNobody} if $value eq 'none'; $value = $self->{parent}->{lang}->{PanelUnknown} if $value eq 'unknown'; } else { $value = GCUtils::encodeEntities($self->transformValue($value, $field)); } $summary .= "\n" .GCUtils::encodeEntities($self->{model}->getDisplayedLabel($field)) .$self->{parent}->{lang}->{Separator} ."" .$value; } return $summary; } } 1;