From 61ebef8f6e8231506f18690389608c93d9736e08 Mon Sep 17 00:00:00 2001 From: Sam Hemelryk Date: Thu, 17 Jun 2010 08:57:00 +0000 Subject: [PATCH] backup MDL-22140 Added conf settings to control the default general backup settings --- admin/settings/courses.php | 22 +++- backup/controller/backup_controller.class.php | 4 + .../dbops/backup_controller_dbops.class.php | 36 +++++++ backup/util/settings/base_setting.class.php | 20 +++- .../settings/setting_dependency.class.php | 42 ++++++++ backup/util/ui/backup_moodleform.class.php | 31 +++--- backup/util/ui/backup_ui_setting.class.php | 36 ++++++- files/index.php | 1 + lang/en/admin.php | 1 + lang/en/backup.php | 27 +++++ lib/adminlib.php | 101 ++++++++++++++++++ pix/i/configlock.gif | Bin 0 -> 138 bytes pix/i/hierarchylock.gif | Bin 0 -> 165 bytes pix/i/permissionlock.gif | Bin 0 -> 167 bytes 14 files changed, 300 insertions(+), 21 deletions(-) create mode 100644 pix/i/configlock.gif create mode 100644 pix/i/hierarchylock.gif create mode 100644 pix/i/permissionlock.gif diff --git a/admin/settings/courses.php b/admin/settings/courses.php index e3b3fad6586..bfa4dfae01c 100644 --- a/admin/settings/courses.php +++ b/admin/settings/courses.php @@ -129,8 +129,26 @@ if ($hassiteconfig $CFG->wwwroot . '/course/pending.php', array('moodle/site:approvecourse'))); } + // Add a category for backups + $ADMIN->add('courses', new admin_category('backups', get_string('backups','admin'))); + + // Create a page for general backup defaults + $temp = new admin_settingpage('backupgeneralsettings', get_string('generalbackdefaults', 'backup'), 'moodle/backup:backupcourse'); + $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_users', get_string('generalusers','backup'), get_string('configgeneralusers','backup'), array('value'=>1, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_anonymize', get_string('generalanonymize','backup'), get_string('configgeneralanonymize','backup'), array('value'=>1, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_role_assignments', get_string('generalroleassignments','backup'), get_string('configgeneralroleassignments','backup'), array('value'=>1, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_user_files', get_string('generaluserfiles','backup'), get_string('configgeneraluserfiles','backup'), array('value'=>1, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_activities', get_string('generalactivities','backup'), get_string('configgeneralactivities','backup'), array('value'=>1, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_blocks', get_string('generalblocks','backup'), get_string('configgeneralblocks','backup'), array('value'=>1, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_filters', get_string('generalfilters','backup'), get_string('configgeneralfilters','backup'), array('value'=>1, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_comments', get_string('generalcomments','backup'), get_string('configgeneralcomments','backup'), array('value'=>1, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_userscompletion', get_string('generaluserscompletion','backup'), get_string('configgeneraluserscompletion','backup'), array('value'=>1, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_logs', get_string('generallogs','backup'), get_string('configgenerallogs','backup'), array('value'=>0, 'locked'=>0))); + $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_histories', get_string('generalhistories','backup'), get_string('configgeneralhistories','backup'), array('value'=>0, 'locked'=>0))); + $ADMIN->add('backups', $temp); + /// "backups" settingpage - $temp = new admin_settingpage('backups', get_string('backups','admin'), 'moodle/backup:backupcourse'); + $temp = new admin_settingpage('scheduled', get_string('scheduledsettings','backup'), 'moodle/backup:backupcourse'); $temp->add(new admin_setting_configcheckbox('backup/backup_sche_modules', get_string('includemodules'), get_string('backupincludemoduleshelp'), 0)); $temp->add(new admin_setting_configcheckbox('backup/backup_sche_withuserdata', get_string('includemoduleuserdata'), get_string('backupincludemoduleuserdatahelp'), 0)); $temp->add(new admin_setting_configcheckbox('backup/backup_sche_metacourse', get_string('metacourse'), get_string('backupmetacoursehelp'), 0)); @@ -166,6 +184,6 @@ if ($hassiteconfig get_string('backupexecuteathelp'), array('h' => 0, 'm' => 0))); $temp->add(new admin_setting_configdirectory('backup/backup_sche_destination', get_string('saveto'), get_string('backupsavetohelp'), '')); - $ADMIN->add('courses', $temp); + $ADMIN->add('backups', $temp); } // end of speedup diff --git a/backup/controller/backup_controller.class.php b/backup/controller/backup_controller.class.php index 0e7326a5d8a..753c6be6595 100644 --- a/backup/controller/backup_controller.class.php +++ b/backup/controller/backup_controller.class.php @@ -236,6 +236,9 @@ class backup_controller extends backup implements loggable { return $this->executiontime; } + /** + * @return backup_plan + */ public function get_plan() { return $this->plan; } @@ -292,6 +295,7 @@ class backup_controller extends backup implements loggable { protected function apply_defaults() { $this->log('applying plan defaults', backup::LOG_DEBUG); + backup_controller_dbops::apply_general_config_defaults($this); $this->set_status(backup::STATUS_CONFIGURED); } } diff --git a/backup/util/dbops/backup_controller_dbops.class.php b/backup/util/dbops/backup_controller_dbops.class.php index dec6bb55c2f..cc91f45052f 100644 --- a/backup/util/dbops/backup_controller_dbops.class.php +++ b/backup/util/dbops/backup_controller_dbops.class.php @@ -344,4 +344,40 @@ abstract class backup_controller_dbops extends backup_dbops { set_config('backup_release', backup::RELEASE); } } + + /** + * Sets the controller settings default values from the backup config. + * + * @param backup_controller $controller + */ + public static function apply_general_config_defaults(backup_controller $controller) { + $settings = array( + // Config name => Setting name + 'backup_general_users' => 'users', + 'backup_general_anonymize' => 'anonymize', + 'backup_general_role_assignments' => 'role_assignments', + 'backup_general_user_files' => 'user_files', + 'backup_general_activities' => 'activities', + 'backup_general_blocks' => 'blocks', + 'backup_general_filters' => 'filters', + 'backup_general_comments' => 'comments', + 'backup_general_userscompletion' => 'userscompletion', + 'backup_general_logs' => 'logs', + 'backup_general_histories' => 'grade_histories' + ); + $plan = $controller->get_plan(); + foreach ($settings as $config=>$settingname) { + $value = get_config('backup', $config); + $locked = (get_config('backup', $config.'_locked') == true); + if ($plan->setting_exists($settingname)) { + $setting = $plan->get_setting($settingname); + if ($setting->get_value() != $value || 1==1) { + $setting->set_value($value); + if ($locked) { + $setting->set_status(base_setting::LOCKED_BY_CONFIG); + } + } + } + } + } } diff --git a/backup/util/settings/base_setting.class.php b/backup/util/settings/base_setting.class.php index 38bf40b1611..557f536f536 100644 --- a/backup/util/settings/base_setting.class.php +++ b/backup/util/settings/base_setting.class.php @@ -170,11 +170,13 @@ abstract class base_setting { public function set_status($status) { $status = $this->validate_status($status); - // If this setting is dependent on other settings first check that all - // of those settings are not locked + // If the setting is being unlocked first check whether an other settings + // this setting is dependent on are locked. If they are then we still don't + // want to lock this setting. if (count($this->dependenton) > 0 && $status == base_setting::NOT_LOCKED) { foreach ($this->dependenton as $dependency) { - if ($dependency->get_setting()->get_status() != base_setting::NOT_LOCKED) { + if ($dependency->is_locked()) { + // It still needs to be locked $status = base_setting::LOCKED_BY_HIERARCHY; break; } @@ -192,7 +194,7 @@ abstract class base_setting { * Gets an array of properties for all of the dependencies that will affect * this setting. * - * This method returns and array rather than the dependencies in order to + * This method returns an array rather than the dependencies in order to * minimise the memory footprint of for the potentially huge recursive * dependency structure that we may be dealing with. * @@ -216,6 +218,16 @@ abstract class base_setting { return $dependencies; } + /** + * Returns all of the dependencies that affect this setting. + * e.g. settings this setting depends on. + * + * @return array Array of setting_dependency's + */ + public function get_settings_depended_on() { + return $this->dependenton; + } + /** * Checks if there are other settings that are dependent on this setting * diff --git a/backup/util/settings/setting_dependency.class.php b/backup/util/settings/setting_dependency.class.php index 7c612f356fd..8e04b4e6450 100644 --- a/backup/util/settings/setting_dependency.class.php +++ b/backup/util/settings/setting_dependency.class.php @@ -142,6 +142,11 @@ abstract class setting_dependency { * @return array */ abstract public function get_moodleform_properties(); + /** + * Returns true if the dependent setting is locked. + * @return bool + */ + abstract public function is_locked(); } /** @@ -169,6 +174,18 @@ class setting_dependency_disabledif_equals extends setting_dependency { parent::__construct($setting, $dependentsetting, $defaultvalue); $this->value = ($value)?(string)$value:0; } + /** + * Returns true if the dependent setting is locked. + * @return bool + */ + public function is_locked() { + // If the setting is locked or the dependent setting should be locked then return true + if ($this->setting->get_status() !== base_setting::NOT_LOCKED || $this->setting->get_value() == $this->value) { + return true; + } + // Else return based upon the dependent settings status + return ($this->dependentsetting->get_status() !== base_setting::NOT_LOCKED); + } /** * Processes a value change in the primary setting * @param mixed $oldvalue @@ -348,6 +365,19 @@ class setting_dependency_disabledif_not_empty extends setting_dependency_disable // Return true if the value has changed for the dependent setting return ($prevalue != $this->dependentsetting->get_value()); } + + /** + * Returns true if the dependent setting is locked. + * @return bool + */ + public function is_locked() { + // If the setting is locked or the dependent setting should be locked then return true + if ($this->setting->get_status() !== base_setting::NOT_LOCKED || !empty($value)) { + return true; + } + // Else return based upon the dependent settings status + return ($this->dependentsetting->get_status() !== base_setting::NOT_LOCKED); + } } /** @@ -400,4 +430,16 @@ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_ // Return true if the value has changed for the dependent setting return ($prevalue != $this->dependentsetting->get_value()); } + /** + * Returns true if the dependent setting is locked. + * @return bool + */ + public function is_locked() { + // If the setting is locked or the dependent setting should be locked then return true + if ($this->setting->get_status() !== base_setting::NOT_LOCKED || empty($value)) { + return true; + } + // Else return based upon the dependent settings status + return ($this->dependentsetting->get_status() !== base_setting::NOT_LOCKED); + } } \ No newline at end of file diff --git a/backup/util/ui/backup_moodleform.class.php b/backup/util/ui/backup_moodleform.class.php index c07b52a102f..ddc552d6c1f 100644 --- a/backup/util/ui/backup_moodleform.class.php +++ b/backup/util/ui/backup_moodleform.class.php @@ -123,16 +123,9 @@ abstract class backup_moodleform extends moodleform { */ function add_setting(backup_setting $setting, backup_task $task=null) { - // Check if the setting is locked first up - if ($setting->get_status() !== base_setting::NOT_LOCKED) { - // If it has no dependencies on other settings we can add it as a - // fixed setting instead - if (!$setting->has_dependencies_on_settings()) { - // Fixed setting it is! - return $this->add_fixed_setting($setting); - } - // Hmm possible to unlock it in the UI so disable instead. - $setting->get_ui()->disable(); + // If the setting cant be changed then add it as a fixed setting. + if (!$setting->get_ui()->is_changeable()) { + return $this->add_fixed_setting($setting); } // First add the formatting for this setting @@ -220,11 +213,21 @@ abstract class backup_moodleform extends moodleform { $settingui = $setting->get_ui(); if ($setting->get_visibility() == backup_setting::VISIBLE) { $this->add_html_formatting($setting); - if ($setting->get_status() != backup_setting::NOT_LOCKED) { - $this->_form->addElement('static', 'static_'.$settingui->get_name(), $settingui->get_label(),$settingui->get_static_value().' '.$OUTPUT->pix_icon('i/unlock', get_string('locked', 'backup'), 'moodle', array('class'=>'smallicon lockedicon'))); - } else { - $this->_form->addElement('static','static_'. $settingui->get_name(), $settingui->get_label(), $settingui->get_static_value()); + switch ($setting->get_status()) { + case backup_setting::LOCKED_BY_PERMISSION: + $icon = ' '.$OUTPUT->pix_icon('i/permissionlock', get_string('lockedbypermission', 'backup'), 'moodle', array('class'=>'smallicon lockedicon permissionlock')); + break; + case backup_setting::LOCKED_BY_CONFIG: + $icon = ' '.$OUTPUT->pix_icon('i/configlock', get_string('lockedbyconfig', 'backup'), 'moodle', array('class'=>'smallicon lockedicon configlock')); + break; + case backup_setting::LOCKED_BY_HIERARCHY: + $icon = ' '.$OUTPUT->pix_icon('i/hierarchylock', get_string('lockedbyhierarchy', 'backup'), 'moodle', array('class'=>'smallicon lockedicon configlock')); + break; + default: + $icon = ''; + break; } + $this->_form->addElement('static', 'static_'.$settingui->get_name(), $settingui->get_label(), $settingui->get_static_value().$icon); $this->_form->addElement('html', html_writer::end_tag('div')); } $this->_form->addElement('hidden', $settingui->get_name(), $settingui->get_value()); diff --git a/backup/util/ui/backup_ui_setting.class.php b/backup/util/ui/backup_ui_setting.class.php index 4d841b64000..d3c3e11f595 100644 --- a/backup/util/ui/backup_ui_setting.class.php +++ b/backup/util/ui/backup_ui_setting.class.php @@ -148,7 +148,6 @@ abstract class backup_setting_ui extends base_setting_ui { public function __construct(backup_setting $setting, $label = null, array $attributes = null, array $options = null) { parent::__construct($setting); // Improve the inputs name by appending the level to the name - if (!($setting instanceof backup_setting)) debug($setting); switch ($setting->get_level()) { case backup_setting::ROOT_LEVEL : $this->name = 'root_'.$setting->get_name(); @@ -230,6 +229,41 @@ abstract class backup_setting_ui extends base_setting_ui { } return $this->label; } + /** + * Returns true if the setting is changeable. + * + * A setting is changeable if it meets either of the two following conditions. + * + * 1. The setting is not locked + * 2. The setting is locked but only by settings that are of the same level (same page) + * + * Condition 2 is really why we have this function + * + * @return bool + */ + public function is_changeable() { + if ($this->setting->get_status() === backup_setting::NOT_LOCKED) { + // Its not locked so its chanegable + return true; + } else if ($this->setting->get_status() !== backup_setting::LOCKED_BY_HIERARCHY) { + // Its not changeable because its locked by permission or config + return false; + } else if ($this->setting->has_dependencies_on_settings()) { + foreach ($this->setting->get_settings_depended_on() as $dependency) { + if ($dependency->is_locked() && $dependency->get_setting()->get_level() !== $this->setting->get_level()) { + // Its not changeable because one or more dependancies arn't + // changeable. + return false; + } + } + // Its changeable because all dependencies are changeable. + return true; + } + // We should never get here but if we do return false to be safe. + // The setting would need to be locked by hierarchy and not have any deps + return false; + } + } /** diff --git a/files/index.php b/files/index.php index ff342761687..fc0776bac45 100644 --- a/files/index.php +++ b/files/index.php @@ -41,6 +41,7 @@ if ($courseid) { } $context = get_context_instance_by_id($contextid, MUST_EXIST); +$PAGE->set_context($context); $course = null; $cm = null; diff --git a/lang/en/admin.php b/lang/en/admin.php index 81b6e55caa9..eb13727d4fc 100755 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -636,6 +636,7 @@ $string['localetext'] = 'Sitewide locale'; $string['localstringcustomization'] = 'Local string customization'; $string['location'] = 'Location'; $string['locationsettings'] = 'Location settings'; +$string['locked'] = 'locked'; $string['log'] = 'Logs'; $string['loginhttps'] = 'Use HTTPS for logins'; $string['loglifetime'] = 'Keep logs for'; diff --git a/lang/en/backup.php b/lang/en/backup.php index 190dc100da0..7ace6bf87c6 100644 --- a/lang/en/backup.php +++ b/lang/en/backup.php @@ -26,6 +26,17 @@ $string['backupcourse'] = 'Backup course: {$a}'; $string['backupsection'] = 'Backup course section: {$a}'; $string['backupactivity'] = 'Backup activity: {$a}'; +$string['configgeneralactivities'] = 'Sets the default for including activities in a backup.'; +$string['configgeneralanonymize'] = 'If enabled all information pertaining to users will be anonymised by default.'; +$string['configgeneralblocks'] = 'Sets the default for including blocks in a backup.'; +$string['configgeneralcomments'] = 'Sets the default for including comments in a backup.'; +$string['configgeneralfilters'] = 'Sets the default for including filters in a backup.'; +$string['configgeneralhistories'] = 'Sets the default for including user history within a backup.'; +$string['configgenerallogs'] = 'If enabled logs will be included in backups by default.'; +$string['configgeneralroleassignments'] = 'If enabled by default roles assignments will also be backed up.'; +$string['configgeneraluserscompletion'] = 'If enabled user completion information will be included in backups by default.'; +$string['configgeneraluserfiles'] = 'Sets the default for whether user files will be included in backups.'; +$string['configgeneralusers'] = 'Sets the default for whether to include users in backups.'; $string['coursesettings'] = 'Course settings'; $string['currentstage1'] = 'Initial settings'; $string['currentstage2'] = 'Schema settings'; @@ -37,10 +48,25 @@ $string['errorfilenamerequired'] = 'You must enter a valid filename for this bac $string['errorfilenamemustbezip'] = 'The filename you enter must be a ZIP file and have the .zip extension'; $string['executionsuccess'] = 'Your backup completed successfully, clicking the continue button below will take you to view your backup file.'; $string['filename'] = 'Filename'; +$string['generalactivities'] = 'Include activities'; +$string['generalanonymize'] = 'Anonymise information'; +$string['generalbackdefaults'] = 'General backup defaults'; +$string['generalblocks'] = 'Include blocks'; +$string['generalcomments'] = 'Include comments'; +$string['generalfilters'] = 'Include filters'; +$string['generalhistories'] = 'Include histories'; +$string['generallogs'] = 'Include logs'; +$string['generalroleassignments'] = 'Include role assignments'; +$string['generaluserscompletion'] = 'Include user completion information'; +$string['generaluserfiles'] = 'Include user files'; +$string['generalusers'] = 'Include users'; $string['includesection'] = 'Include section {$a}'; $string['includeother'] = 'Include {$a}'; $string['includeuserinfo'] = 'Include user information'; $string['locked'] = 'Locked'; +$string['lockedbypermission'] = 'You don\'t have sufficient permissions to change this setting'; +$string['lockedbyconfig'] = 'This setting has been locked by the default backup settings'; +$string['lockedbyhierarchy'] = 'Locked by dependencies'; $string['onstage1action'] = 'Next'; $string['onstage2action'] = 'Next'; $string['onstage4action'] = 'Perform backup'; @@ -48,3 +74,4 @@ $string['onstage8action'] = 'Continue'; $string['onstage16action'] = 'Continue'; $string['previousstage'] = 'Previous'; $string['rootsettings'] = 'Backup settings'; +$string['scheduledsettings'] = 'Scheduled backup settings'; \ No newline at end of file diff --git a/lib/adminlib.php b/lib/adminlib.php index 7f8d29bc7c8..b526150db4b 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -4004,6 +4004,107 @@ EOT; } } +/** + * Checkbox with an advanced checkbox that controls an additional $name.'_locked' config setting. + * + * This is nearly a copy/paste of admin_setting_configcheckbox_with_adv + * + * @copyright 2010 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_setting_configcheckbox_with_lock extends admin_setting_configcheckbox { + /** + * Constructor + * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins. + * @param string $visiblename localised + * @param string $description long localised info + * @param array $defaultsetting ('value'=>string, 'locked'=>bool) + * @param string $yes value used when checked + * @param string $no value used when not checked + */ + public function __construct($name, $visiblename, $description, $defaultsetting, $yes='1', $no='0') { + parent::__construct($name, $visiblename, $description, $defaultsetting, $yes, $no); + } + + /** + * Loads the current setting and returns array + * + * @return array Returns array value=>xx, adv=>xx + */ + public function get_setting() { + $value = parent::get_setting(); + $locked = $this->config_read($this->name.'_locked'); + if (is_null($value) or is_null($locked)) { + return NULL; + } + return array('value' => $value, 'locked' => $locked); + } + + /** + * Sets the value for the setting + * + * Sets the value for the setting to either the yes or no values + * of the object by comparing $data to yes + * + * @param mixed $data Gets converted to str for comparison against yes value + * @return string empty string or error + */ + public function write_setting($data) { + $error = parent::write_setting($data['value']); + if (!$error) { + $value = empty($data['locked']) ? 0 : 1; + $this->config_write($this->name.'_locked', $value); + } + return $error; + } + + /** + * Returns an XHTML checkbox field and with extra locked checkbox + * + * @param string $data If $data matches yes then checkbox is checked + * @param string $query + * @return string XHTML field + */ + public function output_html($data, $query='') { + $defaults = $this->get_defaultsetting(); + $defaultinfo = array(); + if (!is_null($defaults)) { + if ((string)$defaults['value'] === $this->yes) { + $defaultinfo[] = get_string('checkboxyes', 'admin'); + } else { + $defaultinfo[] = get_string('checkboxno', 'admin'); + } + if (!empty($defaults['locked'])) { + $defaultinfo[] = get_string('locked', 'admin'); + } + } + $defaultinfo = implode(', ', $defaultinfo); + + $fullname = $this->get_full_name(); + $novalue = s($this->no); + $yesvalue = s($this->yes); + $id = $this->get_id(); + + $checkboxparams = array('type'=>'checkbox', 'id'=>$id,'name'=>$fullname.'[value]', 'value'=>$yesvalue); + if ((string)$data['value'] === $this->yes) { // convert to strings before comparison + $checkboxparams['checked'] = 'checked'; + } + + $lockcheckboxparams = array('type'=>'checkbox', 'id'=>$id.'_locked','name'=>$fullname.'[locked]', 'value'=>1, 'class'=>'form-checkbox'); + if (!empty($data['locked'])) { // convert to strings before comparison + $lockcheckboxparams['checked'] = 'checked'; + } + + $return = html_writer::start_tag('div', array('class'=>'form-checkbox defaultsnext')); + $return .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>$fullname.'[value]', 'value'=>$novalue)); + $return .= html_writer::empty_tag('input', $checkboxparams); + $return .= html_writer::empty_tag('input', $lockcheckboxparams); + $return .= html_writer::tag('label', get_string('locked', 'admin'), array('for'=>$id.'_locked')); + $return .= html_writer::end_tag('div'); + return format_admin_setting($this, $this->visiblename, $return, $this->description, true, '', $defaultinfo, $query); + } +} + /** * Dropdown menu with an advanced checkbox, that controls a additional $name.'_adv' setting. * diff --git a/pix/i/configlock.gif b/pix/i/configlock.gif new file mode 100644 index 0000000000000000000000000000000000000000..36d1eefef3a27f117be987f31e9eb62df809ffa3 GIT binary patch literal 138 zcmZ?wbhEHb6krfwIK<8X1X}9aIjKCCjxj#I!Ser~*Z=?jUo4MQ{K>+|#lX&>15yD} z%fRekvFi?(fz$V%y$&508anTb<@A(GM!SSPPfLwqU6xogW7@LUhJ8}43pRXjuYvY5GZ{%;grLq6-F+mq1UFKeB-(7g-PmK4+aKn03ES3hX4Qo literal 0 HcmV?d00001 diff --git a/pix/i/hierarchylock.gif b/pix/i/hierarchylock.gif new file mode 100644 index 0000000000000000000000000000000000000000..84bb42405ebfaed6f968765a7fa7d4c9a41e6676 GIT binary patch literal 165 zcmZ?wbhEHb6krfwc+AP5rLO&7T{|b0=YK@R|M~MT9bN=kKj2w;x*1ea5WTfAQ