diff --git a/plugins/ExtendedProfile/ExtendedProfilePlugin.php b/plugins/ExtendedProfile/ExtendedProfilePlugin.php index 3f541c0008..d1572ce9fd 100644 --- a/plugins/ExtendedProfile/ExtendedProfilePlugin.php +++ b/plugins/ExtendedProfile/ExtendedProfilePlugin.php @@ -54,6 +54,7 @@ class ExtendedProfilePlugin extends Plugin function onAutoload($cls) { $lower = strtolower($cls); + switch ($lower) { case 'extendedprofile': @@ -62,6 +63,9 @@ class ExtendedProfilePlugin extends Plugin case 'profiledetailsettingsaction': require_once dirname(__FILE__) . '/' . $lower . '.php'; return false; + case 'userautocompleteaction': + require_once dirname(__FILE__) . '/action/' . mb_substr($lower, 0, -6) . '.php'; + return false; case 'profile_detail': require_once dirname(__FILE__) . '/' . ucfirst($lower) . '.php'; return false; @@ -81,11 +85,19 @@ class ExtendedProfilePlugin extends Plugin */ function onStartInitializeRouter($m) { - $m->connect(':nickname/detail', - array('action' => 'profiledetail'), - array('nickname' => Nickname::DISPLAY_FMT)); - $m->connect('settings/profile/detail', - array('action' => 'profiledetailsettings')); + $m->connect( + ':nickname/detail', + array('action' => 'profiledetail'), + array('nickname' => Nickname::DISPLAY_FMT) + ); + $m->connect( + '/settings/profile/finduser', + array('action' => 'Userautocomplete') + ); + $m->connect( + 'settings/profile/detail', + array('action' => 'profiledetailsettings') + ); return true; } @@ -95,8 +107,6 @@ class ExtendedProfilePlugin extends Plugin $schema = Schema::get(); $schema->ensureTable('profile_detail', Profile_detail::schemaDef()); - // @hack until key definition support is merged - Profile_detail::fixIndexes($schema); return true; } diff --git a/plugins/ExtendedProfile/Profile_detail.php b/plugins/ExtendedProfile/Profile_detail.php index 6fd96cca70..96869b0e63 100644 --- a/plugins/ExtendedProfile/Profile_detail.php +++ b/plugins/ExtendedProfile/Profile_detail.php @@ -21,130 +21,122 @@ if (!defined('STATUSNET')) { exit(1); } -class Profile_detail extends Memcached_DataObject +/** + * DataObject class to store extended profile fields. Allows for storing + * multiple values per a "field_name" (field_name property is not unique). + * + * Example: + * + * Jed's Phone Numbers + * home : 510-384-1992 + * mobile: 510-719-1139 + * work : 415-231-1121 + * + * We can store these phone numbers in a "field" represented by three + * Profile_detail objects, each named 'phone_number' like this: + * + * $phone1 = new Profile_detail(); + * $phone1->field_name = 'phone_number'; + * $phone1->rel = 'home'; + * $phone1->field_value = '510-384-1992'; + * $phone1->value_index = 1; + * + * $phone1 = new Profile_detail(); + * $phone1->field_name = 'phone_number'; + * $phone1->rel = 'mobile'; + * $phone1->field_value = '510-719-1139'; + * $phone1->value_index = 2; + * + * $phone1 = new Profile_detail(); + * $phone1->field_name = 'phone_number'; + * $phone1->rel = 'work'; + * $phone1->field_value = '415-231-1121'; + * $phone1->value_index = 3; + * + */ +class Profile_detail extends Managed_DataObject { - public $__table = 'submirror'; + public $__table = 'profile_detail'; public $id; - - public $profile_id; - public $field; - public $field_index; // relative ordering of multiple values in the same field - - public $value; // primary text value - public $rel; // detail for some field types; eg "home", "mobile", "work" for phones or "aim", "irc", "xmpp" for IM + public $profile_id; // profile this is for + public $rel; // detail for some field types; eg "home", "mobile", "work" for phones or "aim", "irc", "xmpp" for IM + public $field_name; // name + public $field_value; // primary text value + public $value_index; // relative ordering of multiple values in the same field + public $date; // related date public $ref_profile; // for people types, allows pointing to a known profile in the system - public $created; public $modified; - public /*static*/ function staticGet($k, $v=null) + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup + * @param mixed $v Value to lookup + * + * @return User_greeting_count object found, or null for no hits + * + */ + + function staticGet($k, $v=null) { - return parent::staticGet(__CLASS__, $k, $v); + return Memcached_DataObject::staticGet('Profile_detail', $k, $v); } /** - * return table definition for DB_DataObject + * Get an instance by compound key * - * DB_DataObject needs to know something about the table to manipulate - * instances. This method provides all the DB_DataObject needs to know. + * This is a utility method to get a single instance with a given set of + * key-value pairs. Usually used for the primary key for a compound key; thus + * the name. + * + * @param array $kv array of key-value mappings + * + * @return Bookmark object found, or null for no hits * - * @return array array of column definitions */ - function table() + function pkeyGet($kv) { - return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - - 'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'field' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - 'field_index' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - - 'value' => DB_DATAOBJECT_STR, - 'rel' => DB_DATAOBJECT_STR, - 'ref_profile' => DB_DATAOBJECT_ID, - - 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, - 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + return Memcached_DataObject::pkeyGet('Profile_detail', $kv); } static function schemaDef() { - // @fixme need a reverse key on (subscribed, subscriber) as well - return array(new ColumnDef('id', 'integer', - null, false, 'PRI'), - - // @fixme need a unique index on these three - new ColumnDef('profile_id', 'integer', - null, false), - new ColumnDef('field', 'varchar', - 16, false), - new ColumnDef('field_index', 'integer', - null, false), - - new ColumnDef('value', 'text', - null, true), - new ColumnDef('rel', 'varchar', - 16, true), - new ColumnDef('ref_profile', 'integer', - null, true), - - new ColumnDef('created', 'datetime', - null, false), - new ColumnDef('modified', 'datetime', - null, false)); - } - - /** - * Temporary hack to set up the compound index, since we can't do - * it yet through regular Schema interface. (Coming for 1.0...) - * - * @param Schema $schema - * @return void - */ - static function fixIndexes($schema) - { - try { - // @fixme this won't be a unique index... SIGH - $schema->createIndex('profile_detail', array('profile_id', 'field', 'field_index')); - } catch (Exception $e) { - common_log(LOG_ERR, __METHOD__ . ': ' . $e->getMessage()); - } - } - - /** - * return key definitions for DB_DataObject - * - * DB_DataObject needs to know about keys that the table has; this function - * defines them. - * - * @return array key definitions - */ - - function keys() - { - return array_keys($this->keyTypes()); - } - - /** - * return key definitions for Memcached_DataObject - * - * Our caching system uses the same key definitions, but uses a different - * method to get them. - * - * @return array key definitions - */ - - function keyTypes() - { - // @fixme keys - // need a sane key for reverse lookup too - return array('id' => 'K'); - } - - function sequenceKey() - { - return array('id', true); + return array( + 'description' + => 'Additional profile details for the ExtendedProfile plugin', + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true), + 'profile_id' => array('type' => 'int', 'not null' => true), + 'field_name' => array( + 'type' => 'varchar', + 'length' => 16, + 'not null' => true + ), + 'value_index' => array('type' => 'int'), + 'field_value' => array('type' => 'text'), + 'date' => array('type' => 'datetime'), + 'rel' => array('type' => 'varchar', 'length' => 16), + 'rel_profile' => array('type' => 'int'), + 'created' => array( + 'type' => 'datetime', + 'not null' => true + ), + 'modified' => array( + 'type' => 'timestamp', + 'not null' => true + ), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'profile_detail_profile_id_field_name_value_index' + => array('profile_id', 'field_name', 'value_index'), + ) + ); } } diff --git a/plugins/ExtendedProfile/action/userautocomplete.php b/plugins/ExtendedProfile/action/userautocomplete.php new file mode 100644 index 0000000000..d4857429e0 --- /dev/null +++ b/plugins/ExtendedProfile/action/userautocomplete.php @@ -0,0 +1,113 @@ +. + * + * @category Search + * @package StatusNet + * @author Zach Copley + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + + +class UserautocompleteAction extends Action +{ + var $query; + + /** + * Initialization. + * + * @param array $args Web and URL arguments + * + * @return boolean true if nothing goes wrong + */ + function prepare($args) + { + parent::prepare($args); + $this->query = $this->trimmed('term'); + return true; + } + + /** + * Handle a request + * + * @param array $args Arguments from $_REQUEST + * + * @return void + */ + function handle($args) + { + parent::handle($args); + $this->showResults(); + } + + /** + * Search for users matching the query and spit the results out + * as a quick-n-dirty JSON document + * + * @return void + */ + function showResults() + { + $people = array(); + + $profile = new Profile(); + + $search_engine = $profile->getSearchEngine('profile'); + $search_engine->set_sort_mode('nickname_desc'); + $search_engine->limit(0, 10); + $search_engine->query(strtolower($this->query . '*')); + + $cnt = $profile->find(); + + if ($cnt > 0) { + + $sql = 'SELECT profile.* FROM profile, user WHERE profile.id = user.id ' + . ' AND LEFT(LOWER(profile.nickname), ' + . strlen($this->query) + . ') = \'%s\' ' + . ' LIMIT 0, 10'; + + $profile->query(sprintf($sql, $this->query)); + } + + while ($profile->fetch()) { + $people[] = $profile->nickname; + } + + header('Content-Type: application/json; charset=utf-8'); + print json_encode($people); + } + + /** + * Do we need to write to the database? + * + * @return boolean true + */ + function isReadOnly($args) + { + return true; + } +} diff --git a/plugins/ExtendedProfile/css/profiledetail.css b/plugins/ExtendedProfile/css/profiledetail.css new file mode 100644 index 0000000000..3af9bcba4a --- /dev/null +++ b/plugins/ExtendedProfile/css/profiledetail.css @@ -0,0 +1,164 @@ +/* Note the #content is only needed to override weird crap in default styles */ + +#profiledetail .entity_actions { + margin-top: 0px; + margin-bottom: 0px; +} + +#profiledetail #content h3 { + margin-bottom: 5px; +} + +#content table.extended-profile { + width: 100%; + border-collapse: separate; + border-spacing: 0px 8px; + margin-bottom: 10px; +} + +#content table.extended-profile th { + color: #777; + background-color: #ECECF2; + width: 150px; + text-align: right; + padding: 2px 8px 2px 0px; +} + +#content table.extended-profile th.employer, #content table.extended-profile th.institution { + display: none; +} + +#content table.extended-profile td { + padding: 2px 0px 2px 8px; +} + +.experience-item, .education-item { + float: left; + padding-bottom: 4px; +} + +.experience-item .label, .education-item .label { + float: left; + clear: left; + position: relative; + left: -8px; + margin-right: 2px; + margin-bottom: 8px; + color: #777; + background-color: #ECECF2; + width: 150px; + text-align: right; + padding: 2px 8px 2px 0px; +} + +.experience-item .field, .education-item .field { + float: left; + padding-top: 2px; + padding-bottom: 2px; +} + +#profiledetailsettings #content table.extended-profile td { + padding: 0px 0px 0px 8px; +} + +#profiledetailsettings input { + margin-right: 8px; +} + +.form_settings .extended-profile label { + display: none; +} + +.extended-profile textarea { + width: 280px; +} + +.extended-profile input[type=text] { + width: 280px; +} + +.extended-profile .phone-item input[type=text], .extended-profile .im-item input[type=text], .extended-profile .website-item input[type=text] { + width: 175px; +} + +.extended-profile input.hasDatepicker { + width: 100px; +} + +.experience-item input[type=text], .education-item input[type=text] { + float: left; +} + +.extended-profile .current-checkbox { + float: left; + position: relative; + top: 2px; +} + +.form_settings .extended-profile input.checkbox { + margin-left: 0px; + left: 0px; + top: 2px; +} + +.form_settings .extended-profile label.checkbox { + max-width: 100%; + float: none; + display: inline; + left: -20px; +} + +.extended-profile select { + padding-right: 2px; + font-size: 0.88em; +} + +.extended-profile a.add_row, .extended-profile a.remove_row { + display: block; + height: 16px; + width: 16px; + overflow: hidden; + background-image: url('../../../theme/rebase/images/icons/icons-01.gif'); + background-repeat: no-repeat; +} + +.extended-profile a.remove_row { + background-position: 0px -1252px; + float: right; + position: relative; + top: 6px; + line-height: 4em; +} + +.extended-profile a.add_row { + clear: both; + position: relative; + top: 6px; + left: 2px; + background-position: 0px -1186px; + width: 120px; + padding-left: 20px; + line-height: 1.2em; +} + +#content table.extended-profile .supersizeme th { + border-bottom: 28px solid #fff; +} + +#profiledetailsettings .experience-item, #profiledetailsettings .education-item { + margin-bottom: 10px; + width: 100%; +} + +#profiledetailsettings .education-item textarea { + float: left; + margin-bottom: 8px; +} + +#profiledetailsettings tr:last-child .experience-item, #profiledetailsettings tr:last-child .education-item { + margin-bottom: 0px; +} + +#profiledetailsettings .experience-item a.add_row, #profiledetailsettings .education-item a.add_row { + left: 160px; +} diff --git a/plugins/ExtendedProfile/extendedprofile.php b/plugins/ExtendedProfile/extendedprofile.php index 7f69f90899..fa632e5073 100644 --- a/plugins/ExtendedProfile/extendedprofile.php +++ b/plugins/ExtendedProfile/extendedprofile.php @@ -21,27 +21,256 @@ if (!defined('STATUSNET')) { exit(1); } +/** + * Class to represent extended profile data + */ class ExtendedProfile { + protected $fields; + + /** + * Constructor + * + * @param Profile $profile + */ function __construct(Profile $profile) { - $this->profile = $profile; + $this->profile = $profile; + $this->user = $profile->getUser(); + $this->fields = $this->loadFields(); $this->sections = $this->getSections(); - $this->fields = $this->loadFields(); + //common_debug(var_export($this->sections, true)); + + //common_debug(var_export($this->fields, true)); } + /** + * Load extended profile fields + * + * @return array $fields the list of fields + */ function loadFields() { $detail = new Profile_detail(); $detail->profile_id = $this->profile->id; $detail->find(); - - while ($detail->get()) { - $fields[$detail->field][] = clone($detail); + + $fields = array(); + + while ($detail->fetch()) { + $fields[$detail->field_name][] = clone($detail); } + return $fields; } + /** + * Get a the self-tags associated with this profile + * + * @return string the concatenated string of tags + */ + function getTags() + { + return implode(' ', $this->user->getSelfTags()); + } + + /** + * Return a simple string value. Checks for fields that should + * be stored in the regular profile and returns values from it + * if appropriate. + * + * @param string $name name of the detail field to get the + * value from + * + * @return string the value + */ + function getTextValue($name) + { + $key = strtolower($name); + $profileFields = array('fullname', 'location', 'bio'); + + if (in_array($key, $profileFields)) { + return $this->profile->$name; + } else if (array_key_exists($key, $this->fields)) { + return $this->fields[$key][0]->field_value; + } else { + return null; + } + } + + function getDateValue($name) { + $key = strtolower($name); + if (array_key_exists($key, $this->fields)) { + return $this->fields[$key][0]->date; + } else { + return null; + } + } + + // XXX: getPhones, getIms, and getWebsites pretty much do the same thing, + // so refactor. + function getPhones() + { + $phones = (isset($this->fields['phone'])) ? $this->fields['phone'] : null; + $pArrays = array(); + + if (empty($phones)) { + $pArrays[] = array( + 'label' => _m('Phone'), + 'index' => 0, + 'type' => 'phone', + 'vcard' => 'tel', + 'rel' => 'office', + 'value' => null + ); + } else { + for ($i = 0; $i < sizeof($phones); $i++) { + $pa = array( + 'label' => _m('Phone'), + 'type' => 'phone', + 'index' => intval($phones[$i]->value_index), + 'rel' => $phones[$i]->rel, + 'value' => $phones[$i]->field_value, + 'vcard' => 'tel' + ); + + $pArrays[] = $pa; + } + } + return $pArrays; + } + + function getIms() + { + $ims = (isset($this->fields['im'])) ? $this->fields['im'] : null; + $iArrays = array(); + + if (empty($ims)) { + $iArrays[] = array( + 'label' => _m('IM'), + 'type' => 'im' + ); + } else { + for ($i = 0; $i < sizeof($ims); $i++) { + $ia = array( + 'label' => _m('IM'), + 'type' => 'im', + 'index' => intval($ims[$i]->value_index), + 'rel' => $ims[$i]->rel, + 'value' => $ims[$i]->field_value, + ); + + $iArrays[] = $ia; + } + } + return $iArrays; + } + + function getWebsites() + { + $sites = (isset($this->fields['website'])) ? $this->fields['website'] : null; + $wArrays = array(); + + if (empty($sites)) { + $wArrays[] = array( + 'label' => _m('Website'), + 'type' => 'website' + ); + } else { + for ($i = 0; $i < sizeof($sites); $i++) { + $wa = array( + 'label' => _m('Website'), + 'type' => 'website', + 'index' => intval($sites[$i]->value_index), + 'rel' => $sites[$i]->rel, + 'value' => $sites[$i]->field_value, + ); + + $wArrays[] = $wa; + } + } + return $wArrays; + } + + function getExperiences() + { + $companies = (isset($this->fields['company'])) ? $this->fields['company'] : null; + $start = (isset($this->fields['start'])) ? $this->fields['start'] : null; + $end = (isset($this->fields['end'])) ? $this->fields['end'] : null; + + $eArrays = array(); + + if (empty($companies)) { + $eArrays[] = array( + 'label' => _m('Employer'), + 'type' => 'experience', + 'company' => null, + 'start' => null, + 'end' => null, + 'current' => false, + 'index' => 0 + ); + } else { + for ($i = 0; $i < sizeof($companies); $i++) { + $ea = array( + 'label' => _m('Employer'), + 'type' => 'experience', + 'company' => $companies[$i]->field_value, + 'index' => intval($companies[$i]->value_index), + 'current' => $end[$i]->rel, + 'start' => $start[$i]->date, + 'end' => $end[$i]->date + ); + $eArrays[] = $ea; + } + } + return $eArrays; + } + + function getEducation() + { + $schools = (isset($this->fields['school'])) ? $this->fields['school'] : null; + $degrees = (isset($this->fields['degree'])) ? $this->fields['degree'] : null; + $descs = (isset($this->fields['degree_descr'])) ? $this->fields['degree_descr'] : null; + $start = (isset($this->fields['school_start'])) ? $this->fields['school_start'] : null; + $end = (isset($this->fields['school_end'])) ? $this->fields['school_end'] : null; + $iArrays = array(); + + if (empty($schools)) { + $iArrays[] = array( + 'type' => 'education', + 'label' => _m('Institution'), + 'school' => null, + 'degree' => null, + 'description' => null, + 'start' => null, + 'end' => null, + 'index' => 0 + ); + } else { + for ($i = 0; $i < sizeof($schools); $i++) { + $ia = array( + 'type' => 'education', + 'label' => _m('Institution'), + 'school' => $schools[$i]->field_value, + 'degree' => isset($degrees[$i]->field_value) ? $degrees[$i]->field_value : null, + 'description' => isset($descs[$i]->field_value) ? $descs[$i]->field_value : null, + 'index' => intval($schools[$i]->value_index), + 'start' => $start[$i]->date, + 'end' => $end[$i]->date + ); + $iArrays[] = $ia; + } + } + + return $iArrays; + } + + /** + * Return all the sections of the extended profile + * + * @return array the big list of sections and fields + */ function getSections() { return array( @@ -81,22 +310,9 @@ class ExtendedProfile 'contact' => array( 'label' => _m('Contact'), 'fields' => array( - 'phone' => array( - 'label' => _m('Phone'), - 'type' => 'phone', - 'multi' => true, - 'vcard' => 'tel', - ), - 'im' => array( - 'label' => _m('IM'), - 'type' => 'im', - 'multi' => true, - ), - 'website' => array( - 'label' => _m('Websites'), - 'type' => 'website', - 'multi' => true, - ), + 'phone' => $this->getPhones(), + 'im' => $this->getIms(), + 'website' => $this->getWebsites() ), ), 'personal' => array( @@ -119,19 +335,13 @@ class ExtendedProfile 'experience' => array( 'label' => _m('Work experience'), 'fields' => array( - 'experience' => array( - 'type' => 'experience', - 'label' => _m('Employer'), - ), + 'experience' => $this->getExperiences() ), ), 'education' => array( 'label' => _m('Education'), 'fields' => array( - 'education' => array( - 'type' => 'education', - 'label' => _m('Institution'), - ), + 'education' => $this->getEducation() ), ), ); diff --git a/plugins/ExtendedProfile/extendedprofilewidget.php b/plugins/ExtendedProfile/extendedprofilewidget.php index bf9b4056cd..1ef6440ed6 100644 --- a/plugins/ExtendedProfile/extendedprofilewidget.php +++ b/plugins/ExtendedProfile/extendedprofilewidget.php @@ -21,13 +21,35 @@ if (!defined('STATUSNET')) { exit(1); } -class ExtendedProfileWidget extends Widget +/** + * Class for outputting a widget to display or edit + * extended profiles + */ +class ExtendedProfileWidget extends Form { - const EDITABLE=true; + const EDITABLE = true; + /** + * The parent profile + * + * @var Profile + */ protected $profile; + + /** + * The extended profile + * + * @var Extended_profile + */ protected $ext; + /** + * Constructor + * + * @param XMLOutputter $out + * @param Profile $profile + * @param boolean $editable + */ public function __construct(XMLOutputter $out=null, Profile $profile=null, $editable=false) { parent::__construct($out); @@ -38,7 +60,37 @@ class ExtendedProfileWidget extends Widget $this->editable = $editable; } + /** + * Show the extended profile, or the edit form + */ public function show() + { + if ($this->editable) { + parent::show(); + } else { + $this->showSections(); + } + } + + /** + * Show form data + */ + public function formData() + { + // For JQuery UI modal dialog + $this->out->elementStart( + 'div', + array('id' => 'confirm-dialog', 'title' => 'Confirmation Required') + ); + $this->out->text('Really delete this entry?'); + $this->out->elementEnd('div'); + $this->showSections(); + } + + /** + * Show each section of the extended profile + */ + public function showSections() { $sections = $this->ext->getSections(); foreach ($sections as $name => $section) { @@ -46,21 +98,45 @@ class ExtendedProfileWidget extends Widget } } + /** + * Show an extended profile section + * + * @param string $name name of the section + * @param array $section array of fields for the section + */ protected function showExtendedProfileSection($name, $section) { $this->out->element('h3', null, $section['label']); $this->out->elementStart('table', array('class' => 'extended-profile')); + foreach ($section['fields'] as $fieldName => $field) { - $this->showExtendedProfileField($fieldName, $field); + + switch($fieldName) { + case 'phone': + case 'im': + case 'website': + case 'experience': + case 'education': + $this->showMultiple($fieldName, $field); + break; + default: + $this->showExtendedProfileField($fieldName, $field); + } } $this->out->elementEnd('table'); } + /** + * Show an extended profile field + * + * @param string $name name of the field + * @param array $field set of key/value pairs for the field + */ protected function showExtendedProfileField($name, $field) { $this->out->elementStart('tr'); - $this->out->element('th', null, $field['label']); + $this->out->element('th', str_replace(' ','_',strtolower($field['label'])), $field['label']); $this->out->elementStart('td'); if ($this->editable) { @@ -73,30 +149,504 @@ class ExtendedProfileWidget extends Widget $this->out->elementEnd('tr'); } - protected function showFieldValue($name, $field) - { - $this->out->text($name); + protected function showMultiple($name, $fields) { + foreach ($fields as $field) { + $this->showExtendedProfileField($name, $field); + } } + // XXX: showPhone, showIm and showWebsite all work the same, so + // combine + protected function showPhone($name, $field) + { + $this->out->elementStart('div', array('class' => 'phone-display')); + $this->out->text($field['value']); + if (!empty($field['rel'])) { + $this->out->text(' (' . $field['rel'] . ')'); + } + $this->out->elementEnd('div'); + } + + protected function showIm($name, $field) + { + $this->out->elementStart('div', array('class' => 'im-display')); + $this->out->text($field['value']); + if (!empty($field['rel'])) { + $this->out->text(' (' . $field['rel'] . ')'); + } + $this->out->elementEnd('div'); + } + + protected function showWebsite($name, $field) + { + $this->out->elementStart('div', array('class' => 'website-display')); + + $url = $field['value']; + + $this->out->element( + "a", + array( + 'href' => $url, + 'class' => 'extended-profile-link', + 'target' => "_blank" + ), + $url + ); + + if (!empty($field['rel'])) { + $this->out->text(' (' . $field['rel'] . ')'); + } + $this->out->elementEnd('div'); + } + + protected function showEditableIm($name, $field) + { + $index = isset($field['index']) ? $field['index'] : 0; + $id = "extprofile-$name-$index"; + $rel = $id . '-rel'; + $this->out->elementStart( + 'div', array( + 'id' => $id . '-edit', + 'class' => 'im-item' + ) + ); + $this->out->input( + $id, + null, + isset($field['value']) ? $field['value'] : null + ); + $this->out->dropdown( + $id . '-rel', + 'Type', + array( + 'jabber' => 'Jabber', + 'gtalk' => 'GTalk', + 'aim' => 'AIM', + 'yahoo' => 'Yahoo! Messenger', + 'msn' => 'MSN', + 'skype' => 'Skype', + 'other' => 'Other' + ), + null, + false, + isset($field['rel']) ? $field['rel'] : null + ); + + $this->showMultiControls(); + $this->out->elementEnd('div'); + } + + protected function showEditablePhone($name, $field) + { + $index = isset($field['index']) ? $field['index'] : 0; + $id = "extprofile-$name-$index"; + $rel = $id . '-rel'; + $this->out->elementStart( + 'div', array( + 'id' => $id . '-edit', + 'class' => 'phone-item' + ) + ); + $this->out->input( + $id, + null, + isset($field['value']) ? $field['value'] : null + ); + $this->out->dropdown( + $id . '-rel', + 'Type', + array( + 'office' => 'Office', + 'mobile' => 'Mobile', + 'home' => 'Home', + 'pager' => 'Pager', + 'other' => 'Other' + ), + null, + false, + isset($field['rel']) ? $field['rel'] : null + ); + + $this->showMultiControls(); + $this->out->elementEnd('div'); + } + + protected function showEditableWebsite($name, $field) + { + $index = isset($field['index']) ? $field['index'] : 0; + $id = "extprofile-$name-$index"; + $rel = $id . '-rel'; + $this->out->elementStart( + 'div', array( + 'id' => $id . '-edit', + 'class' => 'website-item' + ) + ); + $this->out->input( + $id, + null, + isset($field['value']) ? $field['value'] : null + ); + $this->out->dropdown( + $id . '-rel', + 'Type', + array( + 'blog' => 'Blog', + 'homepage' => 'Homepage', + 'facebook' => 'Facebook', + 'linkedin' => 'LinkedIn', + 'flickr' => 'Flickr', + 'google' => 'Google Profile', + 'other' => 'Other', + 'twitter' => 'Twitter' + ), + null, + false, + isset($field['rel']) ? $field['rel'] : null + ); + + $this->showMultiControls(); + $this->out->elementEnd('div'); + } + + protected function showExperience($name, $field) + { + $this->out->elementStart('div', 'experience-item'); + $this->out->element('div', 'label', _m('Company')); + + if (!empty($field['company'])) { + $this->out->element('div', 'field', $field['company']); + + $this->out->element('div', 'label', _m('Start')); + $this->out->element( + 'div', + array('class' => 'field date'), + date('j M Y', strtotime($field['start']) + ) + ); + $this->out->element('div', 'label', _m('End')); + $this->out->element( + 'div', + array('class' => 'field date'), + date('j M Y', strtotime($field['end']) + ) + ); + + if ($field['current']) { + $this->out->element( + 'div', + array('class' => 'field current'), + '(' . _m('Current') . ')' + ); + } + } + $this->out->elementEnd('div'); + } + + protected function showEditableExperience($name, $field) + { + $index = isset($field['index']) ? $field['index'] : 0; + $id = "extprofile-$name-$index"; + $this->out->elementStart( + 'div', array( + 'id' => $id . '-edit', + 'class' => 'experience-item' + ) + ); + + $this->out->element('div', 'label', _m('Company')); + $this->out->input( + $id, + null, + isset($field['company']) ? $field['company'] : null + ); + + $this->out->element('div', 'label', _m('Start')); + $this->out->input( + $id . '-start', + null, + isset($field['start']) ? date('j M Y', strtotime($field['start'])) : null + ); + + $this->out->element('div', 'label', _m('End')); + + $this->out->input( + $id . '-end', + null, + isset($field['end']) ? date('j M Y', strtotime($field['end'])) : null + ); + $this->out->hidden( + $id . '-current', + 'false' + ); + $this->out->elementStart('div', 'current-checkbox'); + $this->out->checkbox( + $id . '-current', + _m('Current'), + $field['current'] + ); + $this->out->elementEnd('div'); + + $this->showMultiControls(); + $this->out->elementEnd('div'); + } + + protected function showEducation($name, $field) + { + $this->out->elementStart('div', 'education-item'); + $this->out->element('div', 'label', _m('Institution')); + $this->out->element('div', 'field', $field['school']); + $this->out->element('div', 'label', _m('Degree')); + $this->out->element('div', 'field', $field['degree']); + $this->out->element('div', 'label', _m('Description')); + $this->out->element('div', 'field', $field['description']); + $this->out->element('div', 'label', _m('Start')); + $this->out->element( + 'div', + array('class' => 'field date'), + date('j M Y', strtotime($field['start']) + ) + ); + $this->out->element('div', 'label', _m('End')); + $this->out->element( + 'div', + array('class' => 'field date'), + date('j M Y', strtotime($field['end']) + ) + ); + $this->out->elementEnd('div'); + } + + protected function showEditableEducation($name, $field) + { + $index = isset($field['index']) ? $field['index'] : 0; + $id = "extprofile-$name-$index"; + $this->out->elementStart( + 'div', array( + 'id' => $id . '-edit', + 'class' => 'education-item' + ) + ); + $this->out->element('div', 'label', _m('Institution')); + $this->out->input( + $id, + null, + isset($field['school']) ? $field['school'] : null + ); + + $this->out->element('div', 'label', _m('Degree')); + $this->out->input( + $id . '-degree', + null, + isset($field['degree']) ? $field['degree'] : null + ); + + $this->out->element('div', 'label', _m('Description')); + $this->out->element('div', 'field', $field['description']); + + $this->out->textarea( + $id . '-description', + null, + isset($field['description']) ? $field['description'] : null + ); + + $this->out->element('div', 'label', _m('Start')); + $this->out->input( + $id . '-start', + null, + isset($field['start']) ? date('j M Y', strtotime($field['start'])) : null + ); + + $this->out->element('div', 'label', _m('End')); + $this->out->input( + $id . '-end', + null, + isset($field['end']) ? date('j M Y', strtotime($field['end'])) : null + ); + + $this->showMultiControls(); + $this->out->elementEnd('div'); + } + + function showMultiControls() + { + $this->out->element( + 'a', + array( + 'class' => 'remove_row', + 'href' => 'javascript://', + 'style' => 'display: none;' + ), + '-' + ); + + $this->out->element( + 'a', + array( + 'class' => 'add_row', + 'href' => 'javascript://', + 'style' => 'display: none;' + ), + 'Add another item' + ); + } + + /** + * Outputs the value of a field + * + * @param string $name name of the field + * @param array $field set of key/value pairs for the field + */ + protected function showFieldValue($name, $field) + { + $type = strval(@$field['type']); + + switch($type) + { + case '': + case 'text': + case 'textarea': + $this->out->text($this->ext->getTextValue($name)); + break; + case 'date': + $value = $this->ext->getDateValue($name); + if (!empty($value)) { + $this->out->element( + 'div', + array('class' => 'field date'), + date('j M Y', strtotime($value)) + ); + } + break; + case 'person': + $this->out->text($this->ext->getTextValue($name)); + break; + case 'tags': + $this->out->text($this->ext->getTags()); + break; + case 'phone': + $this->showPhone($name, $field); + break; + case 'website': + $this->showWebsite($name, $field); + break; + case 'im': + $this->showIm($name, $field); + break; + case 'experience': + $this->showExperience($name, $field); + break; + case 'education': + $this->showEducation($name, $field); + break; + default: + $this->out->text("TYPE: $type"); + } + } + + /** + * Show an editable version of the field + * + * @param string $name name fo the field + * @param array $field array of key/value pairs for the field + */ protected function showEditableField($name, $field) { $out = $this->out; - //$out = new HTMLOutputter(); - // @fixme + $type = strval(@$field['type']); $id = "extprofile-" . $name; + $value = 'placeholder'; switch ($type) { - case '': - case 'text': - $out->input($id, null, $value); - break; - case 'textarea': - $out->textarea($id, null, $value); - break; - default: - $out->input($id, null, "TYPE: $type"); + case '': + case 'text': + $out->input($id, null, $this->ext->getTextValue($name)); + break; + case 'date': + $out->input( + $id, + null, + date('j M Y', strtotime($this->ext->getDateValue($name))) + ); + break; + case 'person': + $out->input($id, null, $this->ext->getTextValue($name)); + break; + case 'textarea': + $out->textarea($id, null, $this->ext->getTextValue($name)); + break; + case 'tags': + $out->input($id, null, $this->ext->getTags()); + break; + case 'phone': + $this->showEditablePhone($name, $field); + break; + case 'im': + $this->showEditableIm($name, $field); + break; + case 'website': + $this->showEditableWebsite($name, $field); + break; + case 'experience': + $this->showEditableExperience($name, $field); + break; + case 'education': + $this->showEditableEducation($name, $field); + break; + default: + $out->input($id, null, "TYPE: $type"); } } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit( + 'save', + _m('BUTTON','Save'), + 'submit form_action-secondary', + 'save', + _('Save details') + ); + } + + /** + * ID of the form + * + * @return string ID of the form + */ + + function id() + { + return 'profile-details-' . $this->profile->id; + } + + /** + * class of the form + * + * @return string of the form class + */ + + function formClass() + { + return 'form_profile_details form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('profiledetailsettings'); + } } diff --git a/plugins/ExtendedProfile/js/profiledetail.js b/plugins/ExtendedProfile/js/profiledetail.js new file mode 100644 index 0000000000..99a3f78a43 --- /dev/null +++ b/plugins/ExtendedProfile/js/profiledetail.js @@ -0,0 +1,144 @@ +var SN_EXTENDED = SN_EXTENDED || {}; + +SN_EXTENDED.reorder = function(cls) { + + var divs = $('div[class=' + cls + ']'); + + $(divs).each(function(i, div) { + $(div).find('a.add_row').hide(); + $(div).find('a.remove_row').show(); + SN_EXTENDED.replaceIndex(SN_EXTENDED.rowIndex(div), i); + }); + + var lastDiv = $(divs).last().closest('tr'); + lastDiv.addClass('supersizeme'); + + $(divs).last().find('a.add_row').show(); + + if (divs.length == 1) { + $(divs).find('a.remove_row').fadeOut("slow"); + } +}; + +SN_EXTENDED.rowIndex = function(div) { + var idstr = $(div).attr('id'); + var id = idstr.match(/\d+/); + return id; +}; + +SN_EXTENDED.rowCount = function(cls) { + var divs = $.find('div[class=' + cls + ']'); + return divs.length; +}; + +SN_EXTENDED.replaceIndex = function(elem, oldIndex, newIndex) { + $(elem).find('*').each(function() { + $.each(this.attributes, function(i, attrib) { + var regexp = /extprofile-.*-\d.*/; + var value = attrib.value; + var match = value.match(regexp); + if (match !== null) { + attrib.value = value.replace("-" + oldIndex, "-" + newIndex); + } + }); + }); +} + +SN_EXTENDED.resetRow = function(elem) { + $(elem).find('input, textarea').attr('value', ''); + $(elem).find('input').removeAttr('disabled'); + $(elem).find("select option[value='office']").attr("selected", true); + $(elem).find("input:checkbox").attr('checked', false); + $(elem).find("input[name$=-start], input[name$=-end]").each(function() { + $(this).removeClass('hasDatepicker'); + $(this).datepicker({ dateFormat: 'd M yy' }); + }); +}; + +SN_EXTENDED.addRow = function() { + var div = $(this).closest('div'); + var id = div.attr('id'); + var cls = div.attr('class'); + var index = id.match(/\d+/); + var newIndex = parseInt(index) + 1; + var newtr = $(div).closest('tr').removeClass('supersizeme').clone(); + SN_EXTENDED.replaceIndex(newtr, index, newIndex); + SN_EXTENDED.resetRow(newtr); + $(div).closest('tr').after(newtr); + SN_EXTENDED.reorder(cls); +}; + +SN_EXTENDED.removeRow = function() { + + var div = $(this).closest('div'); + var id = $(div).attr('id'); + var cls = $(div).attr('class'); + var that = this; + + $("#confirm-dialog").dialog({ + buttons : { + "Confirm" : function() { + $(this).dialog("close"); + var target = $(that).closest('tr'); + target.fadeOut("slow", function() { + $(target).remove(); + SN_EXTENDED.reorder(cls); + }); + }, + "Cancel" : function() { + $(this).dialog("close"); + } + } + }); + + var cnt = SN_EXTENDED.rowCount(cls); + + if (cnt > 1) { + $("#confirm-dialog").dialog("open"); + } +}; + +$(document).ready(function() { + + $("#confirm-dialog").dialog({ + autoOpen: false, + modal: true + }); + + $("input#extprofile-manager").autocomplete({ + source: 'finduser', + minLength: 2 }); + + $("input[name$=-start], input[name$=-end], #extprofile-birthday").datepicker({ dateFormat: 'd M yy' }); + + var multifields = ["phone-item", "experience-item", "education-item", "im-item", 'website-item']; + + for (f in multifields) { + SN_EXTENDED.reorder(multifields[f]); + } + + $("input#extprofile-manager").autocomplete({ + source: 'finduser', + minLength: 2 }); + + $('.add_row').live('click', SN_EXTENDED.addRow); + $('.remove_row').live('click', SN_EXTENDED.removeRow); + + $('input:checkbox[name$=current]').each(function() { + var input = $(this).parent().siblings('input[id$=-end]'); + if ($(this).is(':checked')) { + $(input).attr('disabled', 'true'); + } + }); + + $('input:checkbox[name$=current]').live('click', function() { + var input = $(this).parent().siblings('input[id$=-end]'); + if ($(this).is(':checked')) { + $(input).val(''); + $(input).attr('disabled', 'true'); + } else { + $(input).removeAttr('disabled'); + } + }); + +}); diff --git a/plugins/ExtendedProfile/profiledetail.css b/plugins/ExtendedProfile/profiledetail.css deleted file mode 100644 index 836b647a10..0000000000 --- a/plugins/ExtendedProfile/profiledetail.css +++ /dev/null @@ -1,22 +0,0 @@ -/* Note the #content is only needed to override weird crap in default styles */ - -#content table.extended-profile { - width: 100%; - border-collapse: separate; - border-spacing: 8px; -} -#content table.extended-profile th { - color: #777; - background-color: #eee; - width: 150px; - - padding-top: 0; /* override bizarre theme defaults */ - - text-align: right; - padding-right: 8px; -} -#content table.extended-profile td { - padding: 0; /* override bizarre theme defaults */ - - padding-left: 8px; -} \ No newline at end of file diff --git a/plugins/ExtendedProfile/profiledetailaction.php b/plugins/ExtendedProfile/profiledetailaction.php index a4bb12956e..a777a28e03 100644 --- a/plugins/ExtendedProfile/profiledetailaction.php +++ b/plugins/ExtendedProfile/profiledetailaction.php @@ -21,8 +21,9 @@ if (!defined('STATUSNET')) { exit(1); } -class ProfileDetailAction extends ProfileAction +class ProfileDetailAction extends ShowstreamAction { + function isReadOnly($args) { return true; @@ -33,28 +34,18 @@ class ProfileDetailAction extends ProfileAction return $this->profile->getFancyName(); } - function showLocalNav() - { - $nav = new PersonalGroupNav($this); - $nav->show(); - } - function showStylesheets() { parent::showStylesheets(); - $this->cssLink('plugins/ExtendedProfile/profiledetail.css'); + $this->cssLink('plugins/ExtendedProfile/css/profiledetail.css'); return true; } - function handle($args) - { - $this->showPage(); - } - function showContent() { $cur = common_current_user(); if ($cur && $cur->id == $this->profile->id) { // your own page $this->elementStart('div', 'entity_actions'); + $this->elementStart('ul'); $this->elementStart('li', 'entity_edit'); $this->element('a', array('href' => common_local_url('profiledetailsettings'), // TRANS: Link title for link on user profile. @@ -62,6 +53,7 @@ class ProfileDetailAction extends ProfileAction // TRANS: Link text for link on user profile. _m('Edit')); $this->elementEnd('li'); + $this->elementEnd('ul'); $this->elementEnd('div'); } diff --git a/plugins/ExtendedProfile/profiledetailsettingsaction.php b/plugins/ExtendedProfile/profiledetailsettingsaction.php index 77d755c0b0..7b03f247ed 100644 --- a/plugins/ExtendedProfile/profiledetailsettingsaction.php +++ b/plugins/ExtendedProfile/profiledetailsettingsaction.php @@ -21,7 +21,7 @@ if (!defined('STATUSNET')) { exit(1); } -class ProfileDetailSettingsAction extends AccountSettingsAction +class ProfileDetailSettingsAction extends ProfileSettingsAction { function title() @@ -43,13 +43,38 @@ class ProfileDetailSettingsAction extends AccountSettingsAction function showStylesheets() { parent::showStylesheets(); - $this->cssLink('plugins/ExtendedProfile/profiledetail.css'); + $this->cssLink('plugins/ExtendedProfile/css/profiledetail.css'); + $this->cssLink('http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css'); return true; } - function handle($args) + function showScripts() { + parent::showScripts(); + $this->script('plugins/ExtendedProfile/js/profiledetail.js'); + $this->script('http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js'); + return true; + } + + function handlePost() { - $this->showPage(); + // CSRF protection + $token = $this->trimmed('token'); + if (!$token || $token != common_session_token()) { + $this->showForm( + _m( + 'There was a problem with your session token. ' + . 'Try again, please.' + ) + ); + return; + } + + if ($this->arg('save')) { + $this->saveDetails(); + } else { + // TRANS: Message given submitting a form with an unknown action + $this->showForm(_m('Unexpected form submission.')); + } } function showContent() @@ -57,7 +82,554 @@ class ProfileDetailSettingsAction extends AccountSettingsAction $cur = common_current_user(); $profile = $cur->getProfile(); - $widget = new ExtendedProfileWidget($this, $profile, ExtendedProfileWidget::EDITABLE); + $widget = new ExtendedProfileWidget( + $this, + $profile, + ExtendedProfileWidget::EDITABLE + ); $widget->show(); } + + function saveDetails() + { + common_debug(var_export($_POST, true)); + + $user = common_current_user(); + + try { + $this->saveStandardProfileDetails($user); + + $profile = $user->getProfile(); + + $simpleFieldNames = array('title', 'spouse', 'kids', 'manager'); + $dateFieldNames = array('birthday'); + + foreach ($simpleFieldNames as $name) { + $value = $this->trimmed('extprofile-' . $name); + if (!empty($value)) { + $this->saveField($user, $name, $value); + } + } + + foreach ($dateFieldNames as $name) { + $value = $this->trimmed('extprofile-' . $name); + $dateVal = $this->parseDate($name, $value); + $this->saveField( + $user, + $name, + null, + null, + null, + $dateVal + ); + } + + $this->savePhoneNumbers($user); + $this->saveIms($user); + $this->saveWebsites($user); + $this->saveExperiences($user); + $this->saveEducations($user); + + } catch (Exception $e) { + $this->showForm($e->getMessage(), false); + return; + } + + $this->showForm(_('Details saved.'), true); + + } + + function parseDate($fieldname, $datestr, $required = false) + { + if (empty($datestr) && $required) { + $msg = sprintf( + _m('You must supply a date for "%s".'), + $fieldname + ); + throw new Exception($msg); + } else { + $ts = strtotime($datestr); + if ($ts === false) { + throw new Exception( + sprintf( + _m('Invalid date entered for "%s": %s'), + $fieldname, + $ts + ) + ); + } + return common_sql_date($ts); + } + return null; + } + + function savePhoneNumbers($user) { + $phones = $this->findPhoneNumbers(); + $this->removeAll($user, 'phone'); + $i = 0; + foreach($phones as $phone) { + if (!empty($phone['value'])) { + ++$i; + $this->saveField( + $user, + 'phone', + $phone['value'], + $phone['rel'], + $i + ); + } + } + } + + function findPhoneNumbers() { + + // Form vals look like this: + // 'extprofile-phone-1' => '11332', + // 'extprofile-phone-1-rel' => 'mobile', + + $phones = $this->sliceParams('phone', 2); + $phoneArray = array(); + + foreach ($phones as $phone) { + list($number, $rel) = array_values($phone); + $phoneArray[] = array( + 'value' => $number, + 'rel' => $rel + ); + } + + return $phoneArray; + } + + function findIms() { + + // Form vals look like this: + // 'extprofile-im-0' => 'jed', + // 'extprofile-im-0-rel' => 'yahoo', + + $ims = $this->sliceParams('im', 2); + $imArray = array(); + + foreach ($ims as $im) { + list($id, $rel) = array_values($im); + $imArray[] = array( + 'value' => $id, + 'rel' => $rel + ); + } + + return $imArray; + } + + function saveIms($user) { + $ims = $this->findIms(); + $this->removeAll($user, 'im'); + $i = 0; + foreach($ims as $im) { + if (!empty($im['value'])) { + ++$i; + $this->saveField( + $user, + 'im', + $im['value'], + $im['rel'], + $i + ); + } + } + } + + function findWebsites() { + + // Form vals look like this: + + $sites = $this->sliceParams('website', 2); + $wsArray = array(); + + foreach ($sites as $site) { + list($id, $rel) = array_values($site); + $wsArray[] = array( + 'value' => $id, + 'rel' => $rel + ); + } + + return $wsArray; + } + + function saveWebsites($user) { + $sites = $this->findWebsites(); + $this->removeAll($user, 'website'); + $i = 0; + foreach($sites as $site) { + + if (!Validate::uri( + $site['value'], + array('allowed_schemes' => array('http', 'https'))) + ) { + throw new Exception(sprintf(_m('Invalid URL: %s'), $site['value'])); + } + + if (!empty($site['value'])) { + ++$i; + $this->saveField( + $user, + 'website', + $site['value'], + $site['rel'], + $i + ); + } + } + } + + function findExperiences() { + + // Form vals look like this: + // 'extprofile-experience-0' => 'Bozotronix', + // 'extprofile-experience-0-current' => 'true' + // 'extprofile-experience-0-start' => '1/5/10', + // 'extprofile-experience-0-end' => '2/3/11', + + $experiences = $this->sliceParams('experience', 4); + $expArray = array(); + + foreach ($experiences as $exp) { + if (sizeof($experiences) == 4) { + list($company, $current, $end, $start) = array_values($exp); + } else { + $end = null; + list($company, $current, $start) = array_values($exp); + } + if (!empty($company)) { + $expArray[] = array( + 'company' => $company, + 'start' => $this->parseDate('Start', $start, true), + 'end' => ($current == 'false') ? $this->parseDate('End', $end, true) : null, + 'current' => ($current == 'false') ? false : true + ); + } + } + + return $expArray; + } + + function saveExperiences($user) { + common_debug('save experiences'); + $experiences = $this->findExperiences(); + + $this->removeAll($user, 'company'); + $this->removeAll($user, 'start'); + $this->removeAll($user, 'end'); // also stores 'current' + + $i = 0; + foreach($experiences as $experience) { + if (!empty($experience['company'])) { + ++$i; + $this->saveField( + $user, + 'company', + $experience['company'], + null, + $i + ); + + $this->saveField( + $user, + 'start', + null, + null, + $i, + $experience['start'] + ); + + // Save "current" employer indicator in rel + if ($experience['current']) { + $this->saveField( + $user, + 'end', + null, + 'current', // rel + $i + ); + } else { + $this->saveField( + $user, + 'end', + null, + null, + $i, + $experience['end'] + ); + } + + } + } + } + + function findEducations() { + + // Form vals look like this: + // 'extprofile-education-0-school' => 'Pigdog', + // 'extprofile-education-0-degree' => 'BA', + // 'extprofile-education-0-description' => 'Blar', + // 'extprofile-education-0-start' => '05/22/99', + // 'extprofile-education-0-end' => '05/22/05', + + $edus = $this->sliceParams('education', 5); + $eduArray = array(); + + foreach ($edus as $edu) { + list($school, $degree, $description, $end, $start) = array_values($edu); + if (!empty($school)) { + $eduArray[] = array( + 'school' => $school, + 'degree' => $degree, + 'description' => $description, + 'start' => $this->parseDate('Start', $start, true), + 'end' => $this->parseDate('End', $end, true) + ); + } + } + + return $eduArray; + } + + + function saveEducations($user) { + common_debug('save education'); + $edus = $this->findEducations(); + common_debug(var_export($edus, true)); + + $this->removeAll($user, 'school'); + $this->removeAll($user, 'degree'); + $this->removeAll($user, 'degree_descr'); + $this->removeAll($user, 'school_start'); + $this->removeAll($user, 'school_end'); + + $i = 0; + foreach($edus as $edu) { + if (!empty($edu['school'])) { + ++$i; + $this->saveField( + $user, + 'school', + $edu['school'], + null, + $i + ); + $this->saveField( + $user, + 'degree', + $edu['degree'], + null, + $i + ); + $this->saveField( + $user, + 'degree_descr', + $edu['description'], + null, + $i + ); + $this->saveField( + $user, + 'school_start', + null, + null, + $i, + $edu['start'] + ); + + $this->saveField( + $user, + 'school_end', + null, + null, + $i, + $edu['end'] + ); + } + } + } + + function arraySplit($array, $pieces) + { + if ($pieces < 2) { + return array($array); + } + + $newCount = ceil(count($array) / $pieces); + $a = array_slice($array, 0, $newCount); + $b = $this->arraySplit(array_slice($array, $newCount), $pieces - 1); + + return array_merge(array($a), $b); + } + + function findMultiParams($type) { + $formVals = array(); + $target = $type; + foreach ($_POST as $key => $val) { + if (strrpos('extprofile-' . $key, $target) !== false) { + $formVals[$key] = $val; + } + } + return $formVals; + } + + function sliceParams($key, $size) { + $slice = array(); + $params = $this->findMultiParams($key); + ksort($params); + $slice = $this->arraySplit($params, sizeof($params) / $size); + return $slice; + } + + /** + * Save an extended profile field as a Profile_detail + * + * @param User $user the current user + * @param string $name field name + * @param string $value field value + * @param string $rel field rel (type) + * @param int $index index (fields can have multiple values) + * @param date $date related date + */ + function saveField($user, $name, $value, $rel = null, $index = null, $date = null) + { + $profile = $user->getProfile(); + $detail = new Profile_detail(); + + $detail->profile_id = $profile->id; + $detail->field_name = $name; + $detail->value_index = $index; + + $result = $detail->find(true); + + if (empty($result)) { + $detial->value_index = $index; + $detail->rel = $rel; + $detail->field_value = $value; + $detail->date = $date; + $detail->created = common_sql_now(); + $result = $detail->insert(); + if (empty($result)) { + common_log_db_error($detail, 'INSERT', __FILE__); + $this->serverError(_m('Could not save profile details.')); + } + } else { + $orig = clone($detail); + + $detail->field_value = $value; + $detail->rel = $rel; + $detail->date = $date; + + $result = $detail->update($orig); + if (empty($result)) { + common_log_db_error($detail, 'UPDATE', __FILE__); + $this->serverError(_m('Could not save profile details.')); + } + } + + $detail->free(); + } + + function removeAll($user, $name) + { + $profile = $user->getProfile(); + $detail = new Profile_detail(); + $detail->profile_id = $profile->id; + $detail->field_name = $name; + $detail->delete(); + $detail->free(); + } + + /** + * Save fields that should be stored in the main profile object + * + * XXX: There's a lot of dupe code here from ProfileSettingsAction. + * Do not want. + * + * @param User $user the current user + */ + function saveStandardProfileDetails($user) + { + $fullname = $this->trimmed('extprofile-fullname'); + $location = $this->trimmed('extprofile-location'); + $tagstring = $this->trimmed('extprofile-tags'); + $bio = $this->trimmed('extprofile-bio'); + + if ($tagstring) { + $tags = array_map( + 'common_canonical_tag', + preg_split('/[\s,]+/', $tagstring) + ); + } else { + $tags = array(); + } + + foreach ($tags as $tag) { + if (!common_valid_profile_tag($tag)) { + // TRANS: Validation error in form for profile settings. + // TRANS: %s is an invalid tag. + throw new Exception(sprintf(_m('Invalid tag: "%s".'), $tag)); + } + } + + $profile = $user->getProfile(); + + $oldTags = $user->getSelfTags(); + $newTags = array_diff($tags, $oldTags); + + if ($fullname != $profile->fullname + || $location != $profile->location + || !empty($newTags) + || $bio != $profile->bio) { + + $orig = clone($profile); + + $profile->nickname = $user->nickname; + $profile->fullname = $fullname; + $profile->bio = $bio; + $profile->location = $location; + + $loc = Location::fromName($location); + + if (empty($loc)) { + $profile->lat = null; + $profile->lon = null; + $profile->location_id = null; + $profile->location_ns = null; + } else { + $profile->lat = $loc->lat; + $profile->lon = $loc->lon; + $profile->location_id = $loc->location_id; + $profile->location_ns = $loc->location_ns; + } + + $profile->profileurl = common_profile_url($user->nickname); + + $result = $profile->update($orig); + + if ($result === false) { + common_log_db_error($profile, 'UPDATE', __FILE__); + // TRANS: Server error thrown when user profile settings could not be saved. + $this->serverError(_('Could not save profile.')); + return; + } + + // Set the user tags + $result = $user->setSelfTags($tags); + + if (!$result) { + // TRANS: Server error thrown when user profile settings tags could not be saved. + $this->serverError(_('Could not save tags.')); + return; + } + + Event::handle('EndProfileSaveForm', array($this)); + common_broadcast_profile($profile); + } + } + }