From 71db023cf1d768c97ae87fa31737638c28e722cd Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Fri, 1 Sep 2017 11:50:35 +0200 Subject: [PATCH] Synchronized with upstream from git.kolab.org (3.2.x) --- calendar.php | 162 +++++---- calendar_base.js | 39 +- calendar_ui.js | 377 +++++++++----------- composer.json | 9 +- config.inc.php.dist | 2 +- drivers/database/database_driver.php | 2 +- drivers/kolab/kolab_calendar.php | 84 +++-- drivers/kolab/kolab_driver.php | 99 +++-- drivers/kolab/kolab_invitation_calendar.php | 27 +- drivers/kolab/kolab_user_calendar.php | 20 +- lib/calendar_recurrence.php | 2 +- lib/calendar_ui.php | 13 +- localization/en_US.inc | 3 +- localization/fi_FI.inc | 35 ++ localization/sk_SK.inc | 219 ++++++++++++ skins/classic/calendar.css | 1 - skins/classic/templates/calendar.html | 1 - skins/larry/calendar.css | 62 ++-- skins/larry/templates/calendar.html | 1 - skins/larry/templates/eventedit.html | 4 +- skins/larry/templates/itipattend.html | 8 - 21 files changed, 721 insertions(+), 449 deletions(-) diff --git a/calendar.php b/calendar.php index 242fcc0..6018704 100644 --- a/calendar.php +++ b/calendar.php @@ -319,6 +319,7 @@ class calendar extends rcube_plugin $this->rc->output->set_env('timezone', $this->timezone->getName()); $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false); $this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver')); + $this->rc->output->set_env('mscolors', jqueryui::get_color_values()); $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->gettext('roleorganizer')))); $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC); @@ -476,42 +477,40 @@ class calendar extends rcube_plugin // loading driver is expensive, don't do it if not needed $this->load_driver(); - if (!isset($no_override['calendar_default_alarm_type']) || !isset($no_override['calendar_default_alarm_offset'])) { + if (!isset($no_override['calendar_default_alarm_type'])) { if (!$p['current']) { $p['blocks']['view']['content'] = true; return $p; } - $alarm_type = $alarm_offset = ''; - - if (!isset($no_override['calendar_default_alarm_type'])) { - $field_id = 'rcmfd_alarm'; - $select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id)); - $select_type->add($this->gettext('none'), ''); - - foreach ($this->driver->alarm_types as $type) { - $select_type->add($this->rc->gettext(strtolower("alarm{$type}option"), 'libcalendaring'), $type); - } - - $alarm_type = $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')); - } - - if (!isset($no_override['calendar_default_alarm_offset'])) { - $field_id = 'rcmfd_alarm'; - $input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3)); - $select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset')); - - foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) { - $select_offset->add($this->rc->gettext('trigger' . $trigger, 'libcalendaring'), $trigger); - } - - $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); - $alarm_offset = $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]); - } + $field_id = 'rcmfd_alarm'; + $select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id)); + $select_type->add($this->gettext('none'), ''); + foreach ($this->driver->alarm_types as $type) + $select_type->add($this->rc->gettext(strtolower("alarm{$type}option"), 'libcalendaring'), $type); $p['blocks']['view']['options']['alarmtype'] = array( 'title' => html::label($field_id, rcube::Q($this->gettext('defaultalarmtype'))), - 'content' => $alarm_type . ' ' . $alarm_offset, + 'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')), + ); + } + + if (!isset($no_override['calendar_default_alarm_offset'])) { + if (!$p['current']) { + $p['blocks']['view']['content'] = true; + return $p; + } + + $field_id = 'rcmfd_alarm'; + $input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3)); + $select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset')); + foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) + $select_offset->add($this->rc->gettext('trigger' . $trigger, 'libcalendaring'), $trigger); + + $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); + $p['blocks']['view']['options']['alarmoffset'] = array( + 'title' => html::label($field_id . 'value', rcube::Q($this->gettext('defaultalarmoffset'))), + 'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]), ); } @@ -1466,7 +1465,7 @@ class calendar extends rcube_plugin { // Upload progress update if (!empty($_GET['_progress'])) { - $this->rc->upload_progress(); + rcube_upload_progress(); } @set_time_limit(0); @@ -1532,11 +1531,11 @@ class calendar extends rcube_plugin } else { if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { - $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array( - 'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize')))))); + $msg = $this->gettext(array('name' => 'filesizeerror', 'vars' => array( + 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize')))))); } else { - $msg = $this->rc->gettext('fileuploaderror'); + $msg = $this->gettext('fileuploaderror'); } $this->rc->output->command('plugin.import_error', array('message' => $msg)); @@ -1779,11 +1778,11 @@ class calendar extends rcube_plugin // convert link URIs references into structs if (array_key_exists('links', $event)) { - foreach ((array) $event['links'] as $i => $link) { - if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link))) { - $event['links'][$i] = $msgref; - } + foreach ((array)$event['links'] as $i => $link) { + if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link))) { + $event['links'][$i] = $msgref; } + } } // check for organizer in attendees list @@ -1975,8 +1974,8 @@ class calendar extends rcube_plugin private function write_preprocess(&$event, $action) { // convert dates into DateTime objects in user's current timezone - $event['start'] = new DateTime($event['start'], $this->timezone); - $event['end'] = new DateTime($event['end'], $this->timezone); + $event['start'] = new DateTime($event['start'], $this->timezone); + $event['end'] = new DateTime($event['end'], $this->timezone); $event['allday'] = (bool)$event['allday']; // start/end is all we need for 'move' action (#1480) @@ -2026,7 +2025,7 @@ class calendar extends rcube_plugin foreach ((array)$event['attendees'] as $i => $attendee) { if ($attendee['role'] == 'ORGANIZER') $organizer = $i; - if ($attendee['email'] == in_array(strtolower($attendee['email']), $emails)) + if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) $owner = $i; if (!isset($attendee['rsvp'])) $event['attendees'][$i]['rsvp'] = true; @@ -2189,6 +2188,7 @@ class calendar extends rcube_plugin // if the backend has free-busy information $fblist = $this->driver->get_freebusy_list($email, $start, $end); + if (is_array($fblist)) { $status = 'FREE'; @@ -2202,7 +2202,7 @@ class calendar extends rcube_plugin } // let this information be cached for 5min - $this->rc->output->future_expire_header(300); + send_future_expire_header(300); echo $status; exit; @@ -2238,13 +2238,26 @@ class calendar extends rcube_plugin $dts = new DateTime('@'.$start); $dts->setTimezone($this->timezone); } - + $fblist = $this->driver->get_freebusy_list($email, $start, $end); - $slots = array(); - + $slots = ''; + + // prepare freebusy list before use (for better performance) + if (is_array($fblist)) { + foreach ($fblist as $idx => $slot) { + list($from, $to, ) = $slot; + + // check for possible all-day times + if (gmdate('His', $from) == '000000' && gmdate('His', $to) == '235959') { + // shift into the user's timezone for sane matching + $fblist[$idx][0] -= $this->gmt_offset; + $fblist[$idx][1] -= $this->gmt_offset; + } + } + } + // build a list from $start till $end with blocks representing the fb-status for ($s = 0, $t = $start; $t <= $end; $s++) { - $status = self::FREEBUSY_UNKNOWN; $t_end = $t + $interval * 60; $dt = new DateTime('@'.$t); $dt->setTimezone($this->timezone); @@ -2252,16 +2265,10 @@ class calendar extends rcube_plugin // determine attendee's status if (is_array($fblist)) { $status = self::FREEBUSY_FREE; + foreach ($fblist as $slot) { list($from, $to, $type) = $slot; - // check for possible all-day times - if (gmdate('His', $from) == '000000' && gmdate('His', $to) == '235959') { - // shift into the user's timezone for sane matching - $from -= $this->gmt_offset; - $to -= $this->gmt_offset; - } - if ($from < $t_end && $to > $t) { $status = isset($type) ? $type : self::FREEBUSY_BUSY; if ($status == self::FREEBUSY_BUSY) // can't get any worse :-) @@ -2269,9 +2276,12 @@ class calendar extends rcube_plugin } } } - - $slots[$s] = $status; - $times[$s] = $dt->format($strformat); + else { + $status = self::FREEBUSY_UNKNOWN; + } + + // use most compact format, assume $status is one digit/character + $slots .= $status; $t = $t_end; } @@ -2279,7 +2289,7 @@ class calendar extends rcube_plugin $dte->setTimezone($this->timezone); // let this information be cached for 5min - $this->rc->output->future_expire_header(300); + send_future_expire_header(300); echo rcube_output::json_serialize(array( 'email' => $email, @@ -2287,7 +2297,6 @@ class calendar extends rcube_plugin 'end' => $dte->format('c'), 'interval' => $interval, 'slots' => $slots, - 'times' => $times, )); exit; } @@ -2686,15 +2695,47 @@ class calendar extends rcube_plugin $this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1); // if user is logged in... + // FIXME: we should really consider removing this functionality + // it's confusing that it creates/updates an event only for logged-in user + // what if the logged-in user is not the same as the attendee? if ($this->rc->user->ID) { $this->load_driver(); + $invitation = $itip->get_invitation($token); + $existing = $this->driver->get_event($this->event); // save the event to his/her default calendar if not yet present - if (!$this->driver->get_event($this->event) && ($calendar = $this->get_default_calendar($invitation['event']['sensitivity']))) { + if (!$existing && ($calendar = $this->get_default_calendar($invitation['event']['sensitivity']))) { $invitation['event']['calendar'] = $calendar['id']; if ($this->driver->new_event($invitation['event'])) $this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation'); + else + $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error'); + } + else if ($existing + && ($this->event['sequence'] >= $existing['sequence'] || $this->event['changed'] >= $existing['changed']) + && ($calendar = $this->driver->get_calendar($existing['calendar'])) + ) { + $this->event = $invitation['event']; + $this->event['id'] = $existing['id']; + + unset($this->event['comment']); + + // merge attendees status + // e.g. preserve my participant status for regular updates + $this->lib->merge_attendees($this->event, $existing, $status); + + // update attachments list + $event['deleted_attachments'] = true; + + // show me as free when declined (#1670) + if ($status == 'declined') + $this->event['free_busy'] = 'free'; + + if ($this->driver->edit_event($this->event)) + $this->rc->output->command('display_message', $this->gettext(array('name' => 'updatedsuccessfully', 'vars' => array('calendar' => $calendar->get_name()))), 'confirmation'); + else + $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error'); } } } @@ -3340,12 +3381,7 @@ class calendar extends rcube_plugin $tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal'); file_put_contents($tmp_path, $this->get_ical()->export(array($event), '', false, array($this->driver, 'get_attachment_body'))); - $args['attachments'][] = array( - 'path' => $tmp_path, - 'name' => $filename . '.ics', - 'mimetype' => 'text/calendar', - 'size' => filesize($tmp_path), - ); + $args['attachments'][] = array('path' => $tmp_path, 'name' => $filename . '.ics', 'mimetype' => 'text/calendar'); $args['param']['subject'] = $event['title']; } } diff --git a/calendar_base.js b/calendar_base.js index 3f00925..41ae8e5 100644 --- a/calendar_base.js +++ b/calendar_base.js @@ -34,6 +34,7 @@ function rcube_calendar(settings) rcube_libcalendaring.call(this, settings); // member vars + this.ui; this.ui_loaded = false; this.selected_attachment = null; @@ -49,29 +50,29 @@ function rcube_calendar(settings) $.when( $.getScript(rcmail.assets_path('plugins/calendar/calendar_ui.js')), $.getScript(rcmail.assets_path('plugins/calendar/lib/js/fullcalendar.js')), - $.get(rcmail.url('calendar/inlineui'), function(html) { $(document.body).append(html); }, 'html') + $.get(rcmail.url('calendar/inlineui'), function(html){ $(document.body).append(html); }, 'html') ).then(function() { // disable attendees feature (autocompletion and stuff is not initialized) for (var c in rcmail.env.calendars) rcmail.env.calendars[c].attendees = rcmail.env.calendars[c].resources = false; - + me.ui_loaded = true; me.ui = new rcube_calendar_ui(me.settings); me.create_from_mail(uid); // start over }); - return; } - - // get message contents for event dialog - var lock = rcmail.set_busy(true, 'loading'); - rcmail.http_post('calendar/mailtoevent', { - '_mbox': rcmail.env.mailbox, - '_uid': uid - }, lock); + else { + // get message contents for event dialog + var lock = rcmail.set_busy(true, 'loading'); + rcmail.http_post('calendar/mailtoevent', { + '_mbox': rcmail.env.mailbox, + '_uid': uid + }, lock); + } } }; - + // callback function triggered from server with contents for the new event this.mail2event_dialog = function(event) { @@ -90,7 +91,7 @@ function rcube_calendar(settings) rcmail.http_post('calendar/mailimportattach', { _uid: rcmail.env.uid, _mbox: rcmail.env.mailbox, - _part: this.selected_attachment + _part: this.selected_attachment, // _calendar: $('#calendar-attachment-saveto').val(), }, rcmail.set_busy(true, 'itip.savingdata')); } @@ -105,11 +106,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { // register create-from-mail command to message_commands array if (rcmail.env.task == 'mail') { - rcmail.register_command('calendar-create-from-mail', function() { cal.create_from_mail(); }); - rcmail.register_command('attachment-save-calendar', function() { cal.save_to_calendar(); }); - rcmail.addEventListener('plugin.mail2event_dialog', function(p) { cal.mail2event_dialog(p); }); - rcmail.addEventListener('plugin.unlock_saving', function(p) { cal.ui && cal.ui.unlock_saving(); }); - + rcmail.register_command('calendar-create-from-mail', function() { cal.create_from_mail() }); + rcmail.register_command('attachment-save-calendar', function() { cal.save_to_calendar() }); + rcmail.addEventListener('plugin.mail2event_dialog', function(p){ cal.mail2event_dialog(p) }); + rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.ui && cal.ui.unlock_saving(); }); + if (rcmail.env.action != 'show') { rcmail.env.message_commands.push('calendar-create-from-mail'); rcmail.add_element($('')); @@ -129,8 +130,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { } rcmail.register_command('plugin.calendar', function() { rcmail.switch_task('calendar'); }, true); - - rcmail.addEventListener('plugin.ping_url', function(p) { + + rcmail.addEventListener('plugin.ping_url', function(p){ var action = p.action; p.action = p.event = null; new Image().src = rcmail.url(action, p); diff --git a/calendar_ui.js b/calendar_ui.js index 8db9b2e..6144e69 100644 --- a/calendar_ui.js +++ b/calendar_ui.js @@ -38,7 +38,7 @@ function rcube_calendar_ui(settings) this.selected_event = null; this.selected_calendar = null; this.search_request = null; - this.saving_lock = null; + this.saving_lock; this.calendars = {}; this.quickview_sources = []; @@ -197,18 +197,18 @@ function rcube_calendar_ui(settings) { var result = [], strlen = str.length, - q, p, i, chr, last; + q, p, i, char, last; for (q = p = i = 0; i < strlen; i++) { - chr = str.charAt(i); - if (chr == '"' && last != '\\') { + char = str.charAt(i); + if (char == '"' && last != '\\') { q = !q; } - else if (!q && chr == delimiter) { + else if (!q && char == delimiter) { result.push(str.substring(p, i)); p = i + 1; } - last = chr; + last = char; } result.push(str.substr(p)); @@ -285,49 +285,6 @@ function rcube_calendar_ui(settings) else return date.getHours() >= settings['work_start'] && date.getHours() < settings['work_end']; }; - - // check if the event has 'real' attendees, excluding the current user - var has_attendees = function(event) - { - return (event.attendees && event.attendees.length && (event.attendees.length > 1 || String(event.attendees[0].email).toLowerCase() != settings.identity.email)); - }; - - // check if the current user is an attendee of this event - var is_attendee = function(event, role, email) - { - var emails = email ? ';'+email.toLowerCase() : settings.identity.emails; - for (var i=0; event.attendees && i < event.attendees.length; i++) { - if ((!role || event.attendees[i].role == role) && event.attendees[i].email && emails.indexOf(';'+event.attendees[i].email.toLowerCase()) >= 0) - return event.attendees[i]; - } - return false; - }; - - // check if the current user is the organizer - var is_organizer = function(event, email) - { - return is_attendee(event, 'ORGANIZER', email) || !event.id; - }; - - /** - * Check permissions on the given calendar object - */ - var has_permission = function(cal, perm) - { - // multiple chars means "either of" - if (String(perm).length > 1) { - for (var i=0; i < perm.length; i++) { - if (has_permission(cal, perm[i])) - return true; - } - } - - if (cal.rights && String(cal.rights).indexOf(perm) >= 0) { - return true; - } - - return (perm == 'i' && cal.editable) || (perm == 'v' && cal.editable); - } var load_attachment = function(event, att) { @@ -515,13 +472,13 @@ function rcube_calendar_ui(settings) return (j - k); }); - var data, organizer, mystatus = null, rsvp, line, morelink, html = '', overflow = ''; + var data, mystatus = null, rsvp, line, morelink, html = '', overflow = '', + organizer = me.is_organizer(event); + for (var j=0; j < event.attendees.length; j++) { data = event.attendees[j]; if (data.email) { - if (data.role == 'ORGANIZER') - organizer = true; - else if (settings.identity.emails.indexOf(';'+data.email) >= 0) { + if (data.role != 'ORGANIZER' && settings.identity.emails.indexOf(';'+data.email) >= 0) { mystatus = data.status.toLowerCase(); if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp) rsvp = mystatus; @@ -540,7 +497,7 @@ function rcube_calendar_ui(settings) morelink = $('').html(rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1)); } } - + if (html && (event.attendees.length > 1 || !organizer)) { $('#event-attendees').show() .children('.event-text') @@ -570,7 +527,7 @@ function rcube_calendar_ui(settings) .text(rcmail.gettext('status' + mystatus, 'libcalendaring')); } - var show_rsvp = rsvp && !is_organizer(event) && event.status != 'CANCELLED' && has_permission(calendar, 'v'); + var show_rsvp = rsvp && !organizer && event.status != 'CANCELLED' && me.has_permission(calendar, 'v'); $('#event-rsvp')[(show_rsvp ? 'show' : 'hide')](); $('#event-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+mystatus+']').prop('disabled', true); @@ -598,7 +555,7 @@ function rcube_calendar_ui(settings) } }); } - if (!temp && has_permission(calendar, 'td') && event.editable !== false) { + if (!temp && me.has_permission(calendar, 'td') && event.editable !== false) { buttons.push({ text: rcmail.gettext('delete', 'calendar'), 'class': 'delete', @@ -671,8 +628,6 @@ function rcube_calendar_ui(settings) } rcmail.enable_command('event-history', calendar.history) - - rcmail.triggerEvent('calendar-event-dialog', {dialog: $dialog}); }; // event handler for clicks on an attendee link @@ -734,8 +689,12 @@ function rcube_calendar_ui(settings) var invite = $('#edit-attendees-invite').get(0); var comment = $('#edit-attendees-comment'); + // make sure any calendar is selected + if (!calendars.val()) + calendars.val($('option:first', calendars).attr('value')); + invite.checked = settings.itip_notify & 1 > 0; - notify.checked = has_attendees(event) && invite.checked; + notify.checked = me.has_attendees(event) && invite.checked; if (event.allDay) { starttime.val("12:00").hide(); @@ -749,7 +708,7 @@ function rcube_calendar_ui(settings) // set calendar selection according to permissions calendars.find('option').each(function(i, opt) { var cal = me.calendars[opt.value] || {}; - $(opt).prop('disabled', !(cal.editable || (action == 'new' && has_permission(cal, 'i')))) + $(opt).prop('disabled', !(cal.editable || (action == 'new' && me.has_permission(cal, 'i')))) }); // set alarm(s) @@ -781,17 +740,17 @@ function rcube_calendar_ui(settings) $('#edit-recurring-warning').hide(); // init attendees tab - var organizer = !event.attendees || is_organizer(event), + var organizer = !event.attendees || me.is_organizer(event), allow_invitations = organizer || (calendar.owner && calendar.owner == 'anonymous') || settings.invite_shared; event_attendees = []; attendees_list = $('#edit-attendees-table > tbody').html(''); resources_list = $('#edit-resources-table > tbody').html(''); - $('#edit-attendees-notify')[(action != 'new' && allow_invitations && has_attendees(event) && (settings.itip_notify & 2) ? 'show' : 'hide')](); - $('#edit-localchanges-warning')[(action != 'new' && has_attendees(event) && !(allow_invitations || (calendar.owner && is_organizer(event, calendar.owner))) ? 'show' : 'hide')](); + $('#edit-attendees-notify')[(action != 'new' && allow_invitations && me.has_attendees(event) && (settings.itip_notify & 2) ? 'show' : 'hide')](); + $('#edit-localchanges-warning')[(action != 'new' && me.has_attendees(event) && !(allow_invitations || (calendar.owner && me.is_organizer(event, calendar.owner))) ? 'show' : 'hide')](); var load_attendees_tab = function() { - var j, data, reply_selected = 0; + var j, data, organizer_attendee, reply_selected = 0; if (event.attendees) { for (j=0; j < event.attendees.length; j++) { data = event.attendees[j]; @@ -803,6 +762,9 @@ function rcube_calendar_ui(settings) add_attendee(data, !allow_invitations); if (allow_invitations && data.role != 'ORGANIZER' && !data.noreply) reply_selected++; + + if (data.role == 'ORGANIZER') + organizer_attendee = data; } } @@ -818,6 +780,15 @@ function rcube_calendar_ui(settings) return false; } }); + + // In case the user is not the (shared) event organizer we'll add the organizer to the selection list + if (!identity_id && !organizer && organizer_attendee) { + var organizer_name = organizer_attendee.email; + if (organizer_attendee.name) + organizer_name = '"' + organizer_attendee.name + '" <' + organizer_name + '>'; + $('#edit-identities-list').append($('