Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAjay Garg <ajay@activitycentral.com>2012-10-12 12:33:02 (GMT)
committer Ajay Garg <ajay@activitycentral.com>2012-10-15 15:46:37 (GMT)
commitb16993db1deaf85793dd950159c4379067277992 (patch)
tree8e90e30b22162d52d4670263235daccb738ea458
parentb436a2049cdbf6fa9cd4aead9e33b727607aa51d (diff)
Multi-Select; 1-to-N.
Signed-off-by: Ajay Garg <ajay@activitycentral.com>
-rw-r--r--src/jarabe/journal/Makefile.am3
-rw-r--r--src/jarabe/journal/expandedentry.py39
-rw-r--r--src/jarabe/journal/journalactivity.py181
-rw-r--r--src/jarabe/journal/journaltoolbox.py336
-rw-r--r--src/jarabe/journal/journalwindow.py45
-rw-r--r--src/jarabe/journal/keepicon.py11
-rw-r--r--src/jarabe/journal/listmodel.py23
-rw-r--r--src/jarabe/journal/listview.py256
-rw-r--r--src/jarabe/journal/misc.py34
-rw-r--r--src/jarabe/journal/model.py482
-rw-r--r--src/jarabe/journal/palettes.py1024
-rw-r--r--src/jarabe/journal/volumestoolbar.py184
-rw-r--r--src/jarabe/journal/webdavmanager.py312
-rw-r--r--src/jarabe/model/neighborhood.py3
-rw-r--r--src/jarabe/view/buddymenu.py12
-rw-r--r--src/jarabe/view/palettes.py51
16 files changed, 2780 insertions, 216 deletions
diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am
index ba29062..98effcf 100644
--- a/src/jarabe/journal/Makefile.am
+++ b/src/jarabe/journal/Makefile.am
@@ -15,4 +15,5 @@ sugar_PYTHON = \
model.py \
objectchooser.py \
palettes.py \
- volumestoolbar.py
+ volumestoolbar.py \
+ webdavmanager.py
diff --git a/src/jarabe/journal/expandedentry.py b/src/jarabe/journal/expandedentry.py
index 8144980..f26e71f 100644
--- a/src/jarabe/journal/expandedentry.py
+++ b/src/jarabe/journal/expandedentry.py
@@ -160,11 +160,30 @@ class ExpandedEntry(Gtk.EventBox):
self._buddy_list.pack_start(self._create_buddy_list(), False, False,
style.DEFAULT_SPACING)
- description = metadata.get('description', '')
+ # TRANS: Do not translate the """%s""".
+ uploader_nick_text = self.__create_text_description(
+ _('Source XO Nick :: \n%s'), metadata.get('uploader-nick', ''))
+
+ # TRANS: Do not translate the """%s""".
+ uploader_serial_text = self.__create_text_description(
+ _('Source XO Serial Number :: \n%s'), metadata.get('uploader-serial', ''))
+
+ # TRANS: Do not translate the """%s""".
+ misc_info_text = self.__create_text_description(
+ _('Misellaneous Information :: \n%s'), metadata.get('description', ''))
+
+ description = uploader_nick_text + uploader_serial_text + misc_info_text
self._description.get_buffer().set_text(description)
+
tags = metadata.get('tags', '')
self._tags.get_buffer().set_text(tags)
+ def __create_text_description(self, heading, value):
+ if (value == '') or (value is None):
+ return ''
+
+ return ((heading % value) + '\n\n')
+
def _create_keep_icon(self):
keep_icon = KeepIcon()
keep_icon.connect('toggled', self._keep_icon_toggled_cb)
@@ -396,18 +415,20 @@ class ExpandedEntry(Gtk.EventBox):
needs_update = True
if needs_update:
- if self._metadata.get('mountpoint', '/') == '/':
- model.write(self._metadata, update_mtime=False)
- else:
- old_file_path = os.path.join(self._metadata['mountpoint'],
- model.get_file_name(old_title,
- self._metadata['mime_type']))
- model.write(self._metadata, file_path=old_file_path,
- update_mtime=False)
+ from jarabe.journal.journalactivity import get_journal
+ self._metadata['mountpoint'] = \
+ get_journal().get_detail_toolbox().get_mount_point()
+
+ model.update_only_metadata_and_preview_files_and_return_file_paths(self._metadata)
self._update_title_sid = None
def _keep_icon_toggled_cb(self, keep_icon):
+ # If it is a locally-mounted remote-share, return without doing
+ # any processing.
+ if model.is_current_mount_point_for_remote_share(model.DETAIL_VIEW):
+ return
+
if keep_icon.get_active():
self._metadata['keep'] = 1
else:
diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index 4bb68fd..b2ffae0 100644
--- a/src/jarabe/journal/journalactivity.py
+++ b/src/jarabe/journal/journalactivity.py
@@ -19,6 +19,7 @@ import logging
from gettext import gettext as _
import uuid
+from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkX11
@@ -27,7 +28,8 @@ import statvfs
import os
from sugar3.graphics.window import Window
-from sugar3.graphics.alert import ErrorAlert
+from sugar3.graphics.icon import Icon
+from sugar3.graphics.alert import Alert, ErrorAlert, ConfirmationAlert
from sugar3.bundle.bundle import ZipExtractException, RegistrationException
from sugar3 import env
@@ -37,7 +39,9 @@ from gi.repository import SugarExt
from jarabe.model import bundleregistry
from jarabe.journal.journaltoolbox import MainToolbox, DetailToolbox
+from jarabe.journal.journaltoolbox import EditToolbox
from jarabe.journal.listview import ListView
+from jarabe.journal.listmodel import ListModel
from jarabe.journal.detailview import DetailView
from jarabe.journal.volumestoolbar import VolumesToolbar
from jarabe.journal import misc
@@ -46,6 +50,7 @@ from jarabe.journal.objectchooser import ObjectChooser
from jarabe.journal.modalalert import ModalAlert
from jarabe.journal import model
from jarabe.journal.journalwindow import JournalWindow
+from jarabe.journal.journalwindow import show_normal_cursor
J_DBUS_SERVICE = 'org.laptop.Journal'
@@ -56,6 +61,7 @@ _SPACE_TRESHOLD = 52428800
_BUNDLE_ID = 'org.laptop.JournalActivity'
_journal = None
+_mount_point = None
class JournalActivityDBusService(dbus.service.Object):
@@ -124,8 +130,33 @@ class JournalActivity(JournalWindow):
self._list_view = None
self._detail_view = None
self._main_toolbox = None
+ self._edit_toolbox = None
self._detail_toolbox = None
self._volumes_toolbar = None
+ self._editing_mode = False
+ self._alert = Alert()
+
+ self._error_alert = Alert()
+ icon = Icon(icon_name='dialog-ok')
+ self._error_alert.add_button(Gtk.ResponseType.OK, _('Ok'), icon)
+ icon.show()
+
+ self._confirmation_alert = Alert()
+ icon = Icon(icon_name='dialog-cancel')
+ self._confirmation_alert.add_button(Gtk.ResponseType.CANCEL, _('Stop'), icon)
+ icon.show()
+ icon = Icon(icon_name='dialog-ok')
+ self._confirmation_alert.add_button(Gtk.ResponseType.OK, _('Continue'), icon)
+ icon.show()
+
+ self._current_alert = None
+ self.setup_handlers_for_alert_actions()
+
+ self._info_alert = None
+ self._selected_entries = []
+ self._bundle_installation_allowed = True
+
+ set_mount_point('/')
self._setup_main_view()
self._setup_secondary_view()
@@ -151,10 +182,17 @@ class JournalActivity(JournalWindow):
self._check_available_space()
def __volume_error_cb(self, gobject, message, severity):
- alert = ErrorAlert(title=severity, msg=message)
- alert.connect('response', self.__alert_response_cb)
- self.add_alert(alert)
- alert.show()
+ self.update_title_and_message(self._error_alert, severity,
+ message)
+ self._callback = None
+ self._data = None
+ self.update_alert(self._error_alert)
+
+ def _show_alert(self, message, severity):
+ self.__volume_error_cb(None, message, severity)
+
+ def _volume_error_cb(self, gobject, message, severity):
+ self.update_error_alert(severity, message, None, None)
def __alert_response_cb(self, alert, response_id):
self.remove_alert(alert)
@@ -196,11 +234,14 @@ class JournalActivity(JournalWindow):
self._main_toolbox.search_entry.connect('icon-press',
self.__search_icon_pressed_cb)
self._main_toolbox.set_mount_point('/')
+ #search_toolbar.set_mount_point('/')
+ set_mount_point('/')
def _setup_secondary_view(self):
self._secondary_view = Gtk.VBox()
self._detail_toolbox = DetailToolbox()
+ self._detail_toolbox.set_mount_point('/')
self._detail_toolbox.connect('volume-error',
self.__volume_error_cb)
@@ -240,9 +281,16 @@ class JournalActivity(JournalWindow):
self.connect('key-press-event', self._key_press_event_cb)
def show_main_view(self):
- if self.toolbar_box != self._main_toolbox:
- self.set_toolbar_box(self._main_toolbox)
- self._main_toolbox.show()
+ if self._editing_mode:
+ self._toolbox = EditToolbox()
+
+ # TRANS: Do not translate the "%d"
+ self._toolbox.set_total_number_of_entries(self.get_total_number_of_entries())
+ else:
+ self._toolbox = self._main_toolbox
+
+ self.set_toolbar_box(self._toolbox)
+ self._toolbox.show()
if self.canvas != self._main_view:
self.set_canvas(self._main_view)
@@ -277,6 +325,10 @@ class JournalActivity(JournalWindow):
def __volume_changed_cb(self, volume_toolbar, mount_point):
logging.debug('Selected volume: %r.', mount_point)
self._main_toolbox.set_mount_point(mount_point)
+ set_mount_point(mount_point)
+
+ # Also, need to update the mount-point for Detail-View.
+ self._detail_toolbox.set_mount_point(mount_point)
def __model_created_cb(self, sender, **kwargs):
self._check_for_bundle(kwargs['object_id'])
@@ -301,6 +353,9 @@ class JournalActivity(JournalWindow):
self._list_view.update_dates()
def _check_for_bundle(self, object_id):
+ if not self._bundle_installation_allowed:
+ return
+
registry = bundleregistry.get_registry()
metadata = model.get(object_id)
@@ -336,6 +391,9 @@ class JournalActivity(JournalWindow):
metadata['bundle_id'] = bundle.get_bundle_id()
model.write(metadata)
+ def set_bundle_installation_allowed(self, allowed):
+ self._bundle_installation_allowed = allowed
+
def __window_state_event_cb(self, window, event):
logging.debug('window_state_event_cb %r', self)
if event.changed_mask & Gdk.WindowState.ICONIFIED:
@@ -378,6 +436,105 @@ class JournalActivity(JournalWindow):
self.reveal()
self.show_main_view()
+ def switch_to_editing_mode(self, switch):
+ # (re)-switch, only if not already.
+ if (switch) and (not self._editing_mode):
+ self._editing_mode = True
+ self.get_list_view().disable_drag_and_copy()
+ self.show_main_view()
+ elif (not switch) and (self._editing_mode):
+ self._editing_mode = False
+ self.get_list_view().enable_drag_and_copy()
+ self.show_main_view()
+
+ def get_list_view(self):
+ return self._list_view
+
+ def setup_handlers_for_alert_actions(self):
+ self._error_alert.connect('response',
+ self.__check_for_alert_action)
+ self._confirmation_alert.connect('response',
+ self.__check_for_alert_action)
+
+ def __check_for_alert_action(self, alert, response_id):
+ self.hide_alert()
+ if self._callback is not None:
+ GObject.idle_add(self._callback, self._data,
+ response_id)
+
+ def update_title_and_message(self, alert, title, message):
+ alert.props.title = title
+ alert.props.msg = message
+
+ def update_alert(self, alert):
+ if self._current_alert is None:
+ self.add_alert(alert)
+ elif self._current_alert != alert:
+ self.remove_alert(self._current_alert)
+ self.add_alert(alert)
+
+ self.remove_alert(self._current_alert)
+ self.add_alert(alert)
+ self._current_alert = alert
+ self._current_alert.show()
+ show_normal_cursor()
+
+ def hide_alert(self):
+ if self._current_alert is not None:
+ self._current_alert.hide()
+
+ def update_info_alert(self, title, message):
+ self.get_toolbar_box().display_running_status_in_multi_select(title, message)
+
+ def update_error_alert(self, title, message, callback, data):
+ self.update_title_and_message(self._error_alert, title,
+ message)
+ self._callback = callback
+ self._data = data
+ self.update_alert(self._error_alert)
+
+ def update_confirmation_alert(self, title, message, callback,
+ data):
+ self.update_title_and_message(self._confirmation_alert, title,
+ message)
+ self._callback = callback
+ self._data = data
+ self.update_alert(self._confirmation_alert)
+
+ def update_progress(self, fraction):
+ self.get_toolbar_box().update_progress(fraction)
+
+ def get_metadata_list(self, selected_state):
+ metadata_list = []
+
+ list_view_model = self.get_list_view().get_model()
+ for index in range(0, len(list_view_model)):
+ metadata = list_view_model.get_metadata(index)
+ metadata_selected = \
+ list_view_model.get_selected_value(metadata['uid'])
+
+ if ( (selected_state and metadata_selected) or \
+ ((not selected_state) and (not metadata_selected)) ):
+ metadata_list.append(metadata)
+
+ return metadata_list
+
+ def get_total_number_of_entries(self):
+ list_view_model = self.get_list_view().get_model()
+ return len(list_view_model)
+
+ def is_editing_mode_present(self):
+ return self._editing_mode
+
+ def get_volumes_toolbar(self):
+ return self._volumes_toolbar
+
+ def get_toolbar_box(self):
+ return self._toolbox
+
+ def get_detail_toolbox(self):
+ return self._detail_toolbox
+
def get_journal():
global _journal
@@ -389,3 +546,11 @@ def get_journal():
def start():
get_journal()
+
+
+def set_mount_point(mount_point):
+ global _mount_point
+ _mount_point = mount_point
+
+def get_mount_point():
+ return _mount_point
diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py
index 09d8a31..cb19f65 100644
--- a/src/jarabe/journal/journaltoolbox.py
+++ b/src/jarabe/journal/journaltoolbox.py
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from gettext import gettext as _
+from gettext import ngettext
import logging
from datetime import datetime, timedelta
import os
@@ -26,6 +27,7 @@ from gi.repository import GObject
from gi.repository import Gio
import glib
from gi.repository import Gtk
+from gi.repository import Gdk
from sugar3.graphics.palette import Palette
from sugar3.graphics.toolbarbox import ToolbarBox
@@ -45,8 +47,9 @@ from jarabe.journal import misc
from jarabe.journal import model
from jarabe.journal.palettes import ClipboardMenu
from jarabe.journal.palettes import VolumeMenu
-from jarabe.journal import journalwindow
+from jarabe.journal import journalwindow, palettes
+COPY_MENU_HELPER = palettes.get_copy_menu_helper()
_AUTOSEARCH_TIMEOUT = 1000
@@ -74,6 +77,10 @@ class MainToolbox(ToolbarBox):
def __init__(self):
ToolbarBox.__init__(self)
+ self._info_widget = MultiSelectEntriesInfoWidget()
+ self.add(self._info_widget)
+ self._info_widget.hide()
+
self._mount_point = None
self.search_entry = iconentry.IconEntry()
@@ -123,6 +130,12 @@ class MainToolbox(ToolbarBox):
self.refresh_filters()
+ def update_progress(self, fraction):
+ self._info_widget.update_progress(fraction)
+
+ def hide_info_widget(self):
+ self._info_widget.hide()
+
def _get_when_search_combo(self):
when_search = ComboBox()
when_search.append_item(_ACTION_ANYTIME, _('Anytime'))
@@ -390,11 +403,30 @@ class DetailToolbox(ToolbarBox):
separator.show()
erase_button = ToolButton('list-remove')
+ self._erase_button = erase_button
erase_button.set_tooltip(_('Erase'))
erase_button.connect('clicked', self._erase_button_clicked_cb)
self.toolbar.insert(erase_button, -1)
erase_button.show()
+ def set_mount_point(self, mount_point):
+ self._mount_point = mount_point
+ self.set_sensitivity_of_icons()
+
+ def get_mount_point(self):
+ return self._mount_point
+
+ def set_sensitivity_of_icons(self):
+ mount_point = self.get_mount_point()
+ if model.is_mount_point_for_locally_mounted_remote_share(mount_point):
+ sensitivity = False
+ else:
+ sensitivity = True
+
+ self._resume.set_sensitive(sensitivity)
+ self._duplicate.set_sensitive(sensitivity)
+ self._erase_button.set_sensitive(sensitivity)
+
def set_metadata(self, metadata):
self._metadata = metadata
self._refresh_copy_palette()
@@ -452,39 +484,11 @@ class DetailToolbox(ToolbarBox):
palette.menu.remove(menu_item)
menu_item.destroy()
- clipboard_menu = ClipboardMenu(self._metadata)
- clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
- icon_size=Gtk.IconSize.MENU))
- clipboard_menu.connect('volume-error', self.__volume_error_cb)
- palette.menu.append(clipboard_menu)
- clipboard_menu.show()
-
- if self._metadata['mountpoint'] != '/':
- client = GConf.Client.get_default()
- color = XoColor(client.get_string('/desktop/sugar/user/color'))
- journal_menu = VolumeMenu(self._metadata, _('Journal'), '/')
- journal_menu.set_image(Icon(icon_name='activity-journal',
- xo_color=color,
- icon_size=Gtk.IconSize.MENU))
- journal_menu.connect('volume-error', self.__volume_error_cb)
- palette.menu.append(journal_menu)
- journal_menu.show()
-
- volume_monitor = Gio.VolumeMonitor.get()
- icon_theme = Gtk.IconTheme.get_default()
- for mount in volume_monitor.get_mounts():
- if self._metadata['mountpoint'] == mount.get_root().get_path():
- continue
- volume_menu = VolumeMenu(self._metadata, mount.get_name(),
- mount.get_root().get_path())
- for name in mount.get_icon().props.names:
- if icon_theme.has_icon(name):
- volume_menu.set_image(Icon(icon_name=name,
- icon_size=Gtk.IconSize.MENU))
- break
- volume_menu.connect('volume-error', self.__volume_error_cb)
- palette.menu.append(volume_menu)
- volume_menu.show()
+ COPY_MENU_HELPER.insert_copy_to_menu_items(palette.menu,
+ [self._metadata],
+ show_editing_alert=False,
+ show_progress_info_alert=False,
+ batch_mode=False)
def _refresh_duplicate_palette(self):
color = misc.get_icon_color(self._metadata)
@@ -524,6 +528,270 @@ class DetailToolbox(ToolbarBox):
menu_item.show()
+class EditToolbox(ToolbarBox):
+ def __init__(self):
+ ToolbarBox.__init__(self)
+
+ self.toolbar.add(SelectNoneButton())
+ self.toolbar.add(SelectAllButton())
+
+ self.toolbar.add(Gtk.SeparatorToolItem())
+
+ self.toolbar.add(BatchEraseButton())
+ self.toolbar.add(BatchCopyButton())
+
+ self.toolbar.add(Gtk.SeparatorToolItem())
+
+ self._multi_select_info_widget = MultiSelectEntriesInfoWidget()
+ self.toolbar.add(self._multi_select_info_widget)
+
+ self.show_all()
+ self.toolbar.show_all()
+
+ def process_new_selected_entry_in_multi_select(self):
+ GObject.idle_add(self._multi_select_info_widget.update_text,
+ '', '', True, True)
+
+ def process_new_deselected_entry_in_multi_select(self):
+ GObject.idle_add(self._multi_select_info_widget.update_text,
+ '', '', False, True)
+
+ def display_running_status_in_multi_select(self, primary_info,
+ secondary_info):
+ GObject.idle_add(self._multi_select_info_widget.update_text,
+ primary_info, secondary_info,
+ None, None)
+
+ def display_already_selected_entries_status(self):
+ GObject.idle_add(self._multi_select_info_widget.update_text,
+ '', '', True, False)
+
+ def set_total_number_of_entries(self, total):
+ self._multi_select_info_widget.set_total_number_of_entries(total)
+
+ def get_current_entry_number(self):
+ return self._multi_select_info_widget.get_current_entry_number()
+
+ def update_progress(self, fraction):
+ self._multi_select_info_widget.update_progress(fraction)
+
+
+class SelectNoneButton(ToolButton):
+ def __init__(self):
+ ToolButton.__init__(self, 'select-none')
+ self.props.tooltip = _('Deselect all')
+
+ self.connect('clicked', self.__do_deselect_all)
+
+ def __do_deselect_all(self, widget_clicked):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ journal.get_list_view()._selected_entries = 0
+ journal.switch_to_editing_mode(False)
+ journal.get_list_view().inhibit_refresh(False)
+ journal.get_list_view().refresh()
+
+
+class SelectAllButton(ToolButton, palettes.ActionItem):
+ def __init__(self):
+ ToolButton.__init__(self, 'select-all')
+ palettes.ActionItem.__init__(self, '', [],
+ show_editing_alert=False,
+ show_progress_info_alert=False,
+ batch_mode=True,
+ auto_deselect_source_entries=True,
+ need_to_popup_options=False,
+ operate_on_deselected_entries=True,
+ show_not_completed_ops_info=False)
+ self.props.tooltip = _('Select all')
+
+ def _get_actionable_signal(self):
+ return 'clicked'
+
+ def _get_editing_alert_operation(self):
+ return _('Select all')
+
+ def _get_info_alert_title(self):
+ return _('Selecting')
+
+ def _get_post_selection_alert_message_entries_len(self):
+ return self._model_len
+
+ def _get_post_selection_alert_message(self, entries_len):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ return ngettext('You have selected %d entry.',
+ 'You have selected %d entries.',
+ entries_len) % (entries_len,)
+
+ def _operate(self, metadata):
+ # Nothing specific needs to be done.
+ # The checkboxes are unchecked as part of the toggling of any
+ # operation that operates on selected entries.
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class BatchEraseButton(ToolButton, palettes.ActionItem):
+ def __init__(self):
+ ToolButton.__init__(self, 'edit-delete')
+ palettes.ActionItem.__init__(self, '', [],
+ show_editing_alert=True,
+ show_progress_info_alert=True,
+ batch_mode=True,
+ auto_deselect_source_entries=True,
+ need_to_popup_options=False,
+ operate_on_deselected_entries=False,
+ show_not_completed_ops_info=True)
+ self.props.tooltip = _('Erase')
+
+ # De-sensitize Batch-Erase button, for locally-mounted-remote-shares.
+ from jarabe.journal.journalactivity import get_mount_point
+ current_mount_point = get_mount_point()
+
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ self.set_sensitive(False)
+
+ def _get_actionable_signal(self):
+ return 'clicked'
+
+ def _get_editing_alert_title(self):
+ return _('Erase')
+
+ def _get_editing_alert_message(self, entries_len):
+ return ngettext('Do you want to erase %d entry?',
+ 'Do you want to erase %d entries?',
+ entries_len) % (entries_len)
+
+ def _get_editing_alert_operation(self):
+ return _('Erase')
+
+ def _get_info_alert_title(self):
+ return _('Erasing')
+
+ def _operate(self, metadata):
+ model.delete(metadata['uid'])
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class BatchCopyButton(ToolButton, palettes.ActionItem):
+ def __init__(self):
+ ToolButton.__init__(self, 'edit-copy')
+ palettes.ActionItem.__init__(self, '', [],
+ show_editing_alert=True,
+ show_progress_info_alert=True,
+ batch_mode=True,
+ auto_deselect_source_entries=False,
+ need_to_popup_options=True,
+ operate_on_deselected_entries=False,
+ show_not_completed_ops_info=False)
+
+ self.props.tooltip = _('Copy')
+
+ self._metadata_list = None
+ self._fill_and_pop_up_options(None)
+
+ def _get_actionable_signal(self):
+ return 'clicked'
+
+ def _fill_and_pop_up_options(self, widget_clicked):
+ for child in self.props.palette.menu.get_children():
+ self.props.palette.menu.remove(child)
+
+ COPY_MENU_HELPER.insert_copy_to_menu_items(self.props.palette.menu,
+ [],
+ show_editing_alert=True,
+ show_progress_info_alert=True,
+ batch_mode=True)
+ if widget_clicked is not None:
+ self.props.palette.popup(immediate=True, state=1)
+
+
+class MultiSelectEntriesInfoWidget(Gtk.ToolItem):
+ def __init__(self):
+ Gtk.ToolItem.__init__(self)
+
+ self._box = Gtk.VBox()
+ self._selected_entries = 0
+
+ self._label = Gtk.Label()
+ self._box.pack_start(self._label, True, True, 0)
+
+ self._progress_label = Gtk.Label()
+ self._box.pack_start(self._progress_label, True, True, 0)
+
+ self.add(self._box)
+
+ self.show_all()
+ self._box.show_all()
+ self._progress_label.hide()
+
+ def set_total_number_of_entries(self, total):
+ self._total = total
+
+ def update_progress(self, fraction):
+ percent = '%.02f' % (fraction * 100)
+
+ # TRANS: Do not translate %.02f
+ text = '%.02f%% complete' % (fraction * 100)
+ if (str(percent) != '100.00') and (str(percent).endswith('00')):
+ self._progress_label.set_text(text)
+ self._progress_label.show()
+ self.show_all()
+ Gdk.Window.process_all_updates()
+ else:
+ self._progress_label.hide()
+ from jarabe.journal.journalactivity import get_journal
+ if not get_journal().is_editing_mode_present():
+ self.hide()
+
+ def update_text(self, primary_text, secondary_text, special_action,
+ update_selected_entries):
+ # If "special_action" is None,
+ # we need to display the info, conveyed by
+ # "primary_message" and "secondary_message"
+ #
+ # If "special_action" is True,
+ # a new entry has been selected.
+ #
+ # If "special_action" is False,
+ # an enrty has been deselected.
+ if special_action == None:
+ self._label.set_text(primary_text + secondary_text)
+ self._label.show()
+ else:
+ if update_selected_entries:
+ if special_action == True:
+ self._selected_entries = self._selected_entries + 1
+ elif special_action == False:
+ self._selected_entries = self._selected_entries - 1
+
+ # TRANS: Do not translate the two "%d".
+ message = _('Selected %d of %d') % (self._selected_entries,
+ self._total)
+
+ # Only show the "selected x of y" for "Select All", or
+ # "Deselect All", or if the user checked/unchecked a
+ # checkbox.
+ from jarabe.journal.palettes import get_current_action_item
+ current_action_item = get_current_action_item()
+ if current_action_item == None or \
+ isinstance(current_action_item, SelectAllButton) or \
+ isinstance(current_action_item, SelectNoneButton):
+ self._label.set_text(message)
+ self._label.show()
+
+ Gdk.Window.process_all_updates()
+
+ def get_current_entry_number(self):
+ return self._selected_entries
+
+
class SortingButton(ToolButton):
__gtype_name__ = 'JournalSortingButton'
diff --git a/src/jarabe/journal/journalwindow.py b/src/jarabe/journal/journalwindow.py
index 776a495..8fcecaf 100644
--- a/src/jarabe/journal/journalwindow.py
+++ b/src/jarabe/journal/journalwindow.py
@@ -15,6 +15,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+from gi.repository import Gdk
+
from sugar3.graphics.window import Window
_journal_window = None
@@ -31,3 +33,46 @@ class JournalWindow(Window):
def get_journal_window():
return _journal_window
+
+
+def set_widgets_active_state(active_state):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ journal.get_toolbar_box().set_sensitive(active_state)
+ journal.get_list_view().set_sensitive(active_state)
+ journal.get_volumes_toolbar().set_sensitive(active_state)
+
+
+def show_waiting_cursor():
+ # Only show waiting-cursor, if this is the batch-mode.
+
+ from jarabe.journal.journalactivity import get_journal
+ if not get_journal().is_editing_mode_present():
+ return
+
+ _journal_window.get_root_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))
+
+
+def freeze_ui():
+ # Only freeze, if this is the batch-mode.
+
+ from jarabe.journal.journalactivity import get_journal
+ if not get_journal().is_editing_mode_present():
+ return
+
+ show_waiting_cursor()
+
+ set_widgets_active_state(False)
+
+
+def show_normal_cursor():
+ _journal_window.get_root_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR))
+
+
+def unfreeze_ui():
+ # Unfreeze, irrespective of whether this is the batch mode.
+
+ set_widgets_active_state(True)
+
+ show_normal_cursor()
diff --git a/src/jarabe/journal/keepicon.py b/src/jarabe/journal/keepicon.py
index 16e3a57..9c7b7d5 100644
--- a/src/jarabe/journal/keepicon.py
+++ b/src/jarabe/journal/keepicon.py
@@ -22,6 +22,8 @@ from sugar3.graphics.icon import Icon
from sugar3.graphics import style
from sugar3.graphics.xocolor import XoColor
+from jarabe.journal import model
+
class KeepIcon(Gtk.ToggleButton):
def __init__(self):
@@ -37,6 +39,9 @@ class KeepIcon(Gtk.ToggleButton):
self.connect('enter-notify-event', self.__enter_notify_event_cb)
def __toggled_cb(self, widget):
+ if model.is_current_mount_point_for_remote_share(model.DETAIL_VIEW):
+ return
+
if self.get_active():
client = GConf.Client.get_default()
color = XoColor(client.get_string('/desktop/sugar/user/color'))
@@ -47,9 +52,15 @@ class KeepIcon(Gtk.ToggleButton):
self._icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
def __enter_notify_event_cb(self, icon, event):
+ if model.is_current_mount_point_for_remote_share(model.DETAIL_VIEW):
+ return
+
if not self.get_active():
self._icon.props.fill_color = style.COLOR_BUTTON_GREY.get_svg()
def __leave_notify_event_cb(self, icon, event):
+ if model.is_current_mount_point_for_remote_share(model.DETAIL_VIEW):
+ return
+
if not self.get_active():
self._icon.props.fill_color = style.COLOR_TRANSPARENT.get_svg()
diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py
index b98d01c..a5bb7b0 100644
--- a/src/jarabe/journal/listmodel.py
+++ b/src/jarabe/journal/listmodel.py
@@ -54,6 +54,7 @@ class ListModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource):
COLUMN_BUDDY_1 = 9
COLUMN_BUDDY_2 = 10
COLUMN_BUDDY_3 = 11
+ COLUMN_SELECT = 12
_COLUMN_TYPES = {
COLUMN_UID: str,
@@ -68,6 +69,7 @@ class ListModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource):
COLUMN_BUDDY_1: object,
COLUMN_BUDDY_3: object,
COLUMN_BUDDY_2: object,
+ COLUMN_SELECT: bool,
}
_PAGE_SIZE = 10
@@ -79,6 +81,8 @@ class ListModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource):
self._cached_row = None
self._result_set = model.find(query, ListModel._PAGE_SIZE)
self._temp_drag_file_path = None
+ self._selected = {}
+ self._uid_metadata_assoc = {}
# HACK: The view will tell us that it is resizing so the model can
# avoid hitting D-Bus and disk.
@@ -248,3 +252,22 @@ class ListModel(GObject.GObject, Gtk.TreeModel, Gtk.TreeDragSource):
return True
return False
+
+ def update_uid_metadata_assoc(self, uid, metadata):
+ self._uid_metadata_assoc[uid] = metadata
+
+ def set_selected_value(self, uid, value):
+ if value == False:
+ del self._selected[uid]
+ elif value == True:
+ self._selected[uid] = value
+
+ def get_selected_value(self, uid):
+ if self._selected.has_key(uid):
+ return True
+ else:
+ return False
+
+ def get_in_memory_metadata(self, path):
+ uid = self[path][ListModel.COLUMN_UID]
+ return self._uid_metadata_assoc[uid]
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
index 5b2c5ab..35c8092 100644
--- a/src/jarabe/journal/listview.py
+++ b/src/jarabe/journal/listview.py
@@ -67,7 +67,8 @@ class BaseListView(Gtk.Bin):
'clear-clicked': (GObject.SignalFlags.RUN_FIRST, None, ([])),
}
- def __init__(self):
+ def __init__(self, is_object_chooser):
+ self._is_object_chooser = is_object_chooser
self._query = {}
self._model = None
self._progress_bar = None
@@ -100,11 +101,11 @@ class BaseListView(Gtk.Bin):
self._title_column = None
self.sort_column = None
self._add_columns()
+ self._inhibit_refresh = False
+ self._selected_entries = 0
+
+ self.enable_drag_and_copy()
- self.tree_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
- [('text/uri-list', 0, 0),
- ('journal-object-id', 0, 0)],
- Gdk.DragAction.COPY)
# Auto-update stuff
self._fully_obscured = True
@@ -116,6 +117,15 @@ class BaseListView(Gtk.Bin):
model.updated.connect(self.__model_updated_cb)
model.deleted.connect(self.__model_deleted_cb)
+ def enable_drag_and_copy(self):
+ self.tree_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
+ [('text/uri-list', 0, 0),
+ ('journal-object-id', 0, 0)],
+ Gdk.DragAction.COPY)
+
+ def disable_drag_and_copy(self):
+ self.tree_view.unset_rows_drag_source()
+
def __model_created_cb(self, sender, signal, object_id):
if self._is_new_item_visible(object_id):
self._set_dirty()
@@ -136,6 +146,17 @@ class BaseListView(Gtk.Bin):
return object_id.startswith(self._query['mountpoints'][0])
def _add_columns(self):
+ if not self._is_object_chooser:
+ cell_select = CellRendererToggle(self.tree_view)
+ cell_select.connect('clicked', self.__cell_select_clicked_cb)
+
+ column = Gtk.TreeViewColumn()
+ column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
+ column.props.fixed_width = cell_select.props.width
+ column.pack_start(cell_select, True)
+ column.set_cell_data_func(cell_select, self.__select_set_data_cb)
+ self.tree_view.append_column(column)
+
cell_favorite = CellRendererFavorite(self.tree_view)
cell_favorite.connect('clicked', self.__favorite_clicked_cb)
@@ -248,8 +269,30 @@ class BaseListView(Gtk.Bin):
def __favorite_set_data_cb(self, column, cell, tree_model,
tree_iter, data):
- favorite = tree_model[tree_iter][ListModel.COLUMN_FAVORITE]
- if favorite:
+ # Instead of querying the favorite-status from the "cached"
+ # entries in listmodel, hit the DS, and retrieve the persisted
+ # favorite-status.
+ # This solves the issue in "Multi-Select", wherein the
+ # listview is inhibited from refreshing. Now, if the user
+ # clicks favorite-star-icon(s), the change(s) is(are) written
+ # to the DS, but no refresh takes place. Thus, in order to have
+ # the change(s) reflected on the UI, we need to hit the DS for
+ # querying the favorite-status (instead of relying on the
+ # cached-listmodel.
+ uid = tree_model[tree_iter][ListModel.COLUMN_UID]
+ if uid is None:
+ return
+
+ try:
+ metadata = model.get(uid)
+ except:
+ return
+
+ favorite = None
+ if 'keep' in metadata.keys():
+ favorite = str(metadata['keep'])
+
+ if favorite == '1':
client = GConf.Client.get_default()
color = XoColor(client.get_string('/desktop/sugar/user/color'))
cell.props.xo_color = color
@@ -257,6 +300,11 @@ class BaseListView(Gtk.Bin):
cell.props.xo_color = None
def __favorite_clicked_cb(self, cell, path):
+ # If this is a remote-share, return without doing any
+ # processing.
+ if model.is_current_mount_point_for_remote_share(model.LIST_VIEW):
+ return
+
row = self._model[path]
metadata = model.get(row[ListModel.COLUMN_UID])
if not model.is_editable(metadata):
@@ -265,7 +313,94 @@ class BaseListView(Gtk.Bin):
metadata['keep'] = '0'
else:
metadata['keep'] = '1'
- model.write(metadata, update_mtime=False)
+
+ from jarabe.journal.journalactivity import get_mount_point
+ metadata['mountpoint'] = get_mount_point()
+
+ model.update_only_metadata_and_preview_files_and_return_file_paths(metadata)
+ self.__redraw_view_if_necessary()
+
+ def __select_set_data_cb(self, column, cell, tree_model, tree_iter,
+ data):
+ uid = tree_model[tree_iter][ListModel.COLUMN_UID]
+ if uid is None:
+ return
+
+ # Hack to associate the cell with the metadata, so that it (the
+ # cell) is available offline as well (example during
+ # batch-operations, when the processing has to be done, without
+ # actually clicking any cell.
+ try:
+ metadata = model.get(uid)
+ except:
+ # https://dev.laptop.org.au/issues/1119
+ # http://bugs.sugarlabs.org/ticket/3344
+ # Occurs, when copying entries from journal to pen-drive.
+ # Simply swallow the exception, and return, as this too,
+ # like the above case, does not have any impact on the
+ # functionality.
+ return
+
+ metadata['cell'] = cell
+ tree_model.update_uid_metadata_assoc(uid, metadata)
+
+ self.do_ui_select_change(metadata)
+
+ def __cell_select_clicked_cb(self, cell, path):
+ row = self._model[path]
+ treeiter = self._model.get_iter(path)
+ metadata = model.get(row[ListModel.COLUMN_UID])
+ self.do_backend_select_change(metadata)
+
+ def do_ui_select_change(self, metadata):
+ tree_model = self.get_model()
+ selected = tree_model.get_selected_value(metadata['uid'])
+
+ if 'cell' in metadata.keys():
+ cell = metadata['cell']
+ if selected:
+ cell.props.icon_name = 'emblem-checked'
+ else:
+ cell.props.icon_name = 'emblem-unchecked'
+
+ def do_backend_select_change(self, metadata):
+ uid = metadata['uid']
+ selected = self._model.get_selected_value(uid)
+
+ self._model.set_selected_value(uid, not selected)
+ self._process_new_selected_status(not selected)
+
+ def _process_new_selected_status(self, new_status):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+ journal_toolbar_box = journal.get_toolbar_box()
+
+ self.__redraw_view_if_necessary()
+
+ if new_status == False:
+ self._selected_entries = self._selected_entries - 1
+ journal_toolbar_box.process_new_deselected_entry_in_multi_select()
+ GObject.idle_add(self._post_backend_processing)
+ else:
+ self._selected_entries = self._selected_entries + 1
+ journal.get_list_view().inhibit_refresh(True)
+ journal.switch_to_editing_mode(True)
+
+ # For the case, when we are switching to editing-mode.
+ # The previous call won't actually redraw, as we are not in
+ # editing-mode that time.
+ self.__redraw_view_if_necessary()
+
+ journal.get_toolbar_box().process_new_selected_entry_in_multi_select()
+
+ def _post_backend_processing(self):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ if self._selected_entries == 0:
+ journal.switch_to_editing_mode(False)
+ journal.get_list_view().inhibit_refresh(False)
+ journal.get_list_view().refresh()
def update_with_query(self, query_dict):
logging.debug('ListView.update_with_query')
@@ -282,6 +417,11 @@ class BaseListView(Gtk.Bin):
self.refresh()
def refresh(self):
+ if not self._inhibit_refresh:
+ self.set_sensitive(True)
+ self.proceed_with_refresh()
+
+ def proceed_with_refresh(self):
logging.debug('ListView.refresh query %r', self._query)
self._stop_progress_bar()
@@ -482,6 +622,64 @@ class BaseListView(Gtk.Bin):
self.update_dates()
return True
+ def get_model(self):
+ return self._model
+
+ def inhibit_refresh(self, inhibit):
+ self._inhibit_refresh = inhibit
+
+ def __redraw_view_if_necessary(self):
+ from jarabe.journal.journalactivity import get_journal
+ if not get_journal().is_editing_mode_present():
+ return
+
+ # First, get the total number of entries, for which the
+ # batch-operation is under progress.
+ from jarabe.journal.palettes import get_current_action_item
+
+ current_action_item = get_current_action_item()
+ if current_action_item is None:
+ # A single checkbox has been clicked/unclicked.
+ self.__redraw()
+ return
+
+ total_items = current_action_item.get_number_of_entries_to_operate_upon()
+
+ # Then, get the current entry being processed.
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+ current_entry_number = journal.get_toolbar_box().get_current_entry_number()
+
+ # Redraw, if "current_entry_number" is 10.
+ if current_entry_number == 10:
+ self.__log(current_entry_number, total_items)
+ self.__redraw()
+ return
+
+ # Redraw, if this is the last entry.
+ if current_entry_number == total_items:
+ self.__log(current_entry_number, total_items)
+ self.__redraw()
+ return
+
+ # Redraw, if this is the 20% interval.
+ twenty_percent_of_total_items = total_items / 5
+ if twenty_percent_of_total_items < 10:
+ return
+
+ if (current_entry_number % twenty_percent_of_total_items) == 0:
+ self.__log(current_entry_number, total_items)
+ self.__redraw()
+ return
+
+ def __log(self, current_entry_number, total_items):
+ pass
+
+ def __redraw(self):
+ tree_view_window = self.tree_view.get_bin_window()
+ tree_view_window.hide()
+ tree_view_window.show()
+
class ListView(BaseListView):
__gtype_name__ = 'JournalListView'
@@ -497,8 +695,8 @@ class ListView(BaseListView):
([])),
}
- def __init__(self):
- BaseListView.__init__(self)
+ def __init__(self, is_object_chooser=False):
+ BaseListView.__init__(self, is_object_chooser)
self._is_dragging = False
self.tree_view.connect('drag-begin', self.__drag_begin_cb)
@@ -559,11 +757,25 @@ class ListView(BaseListView):
self.emit('volume-error', message, severity)
def __icon_clicked_cb(self, cell, path):
+ # For locally-mounted remote shares, we do not want to launch
+ # by clicking on the icons.
+ # So, check if this is a part of locally-mounted-remote share,
+ # and if yes, return, without doing anything.
+ from jarabe.journal.journalactivity import get_mount_point
+ current_mount_point = get_mount_point()
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ return
+
row = self.tree_view.get_model()[path]
metadata = model.get(row[ListModel.COLUMN_UID])
misc.resume(metadata)
def __cell_title_edited_cb(self, cell, path, new_text):
+ from jarabe.journal.journalactivity import get_journal, \
+ get_mount_point
+ if get_journal().is_editing_mode_present():
+ return
+
row = self._model[path]
metadata = model.get(row[ListModel.COLUMN_UID])
metadata['title'] = new_text
@@ -592,6 +804,25 @@ class CellRendererFavorite(CellRendererIcon):
self.props.prelit_stroke_color = prelit_color.get_stroke_color()
self.props.prelit_fill_color = prelit_color.get_fill_color()
+ def do_render(self, cr, widget, background_area, cell_area, flags):
+ # If this is a remote-share, mask the "PRELIT" flag.
+ if model.is_current_mount_point_for_remote_share(model.LIST_VIEW):
+ flags = flags & (~(Gtk.CellRendererState.PRELIT))
+
+ CellRendererIcon.do_render(self, cr, widget, background_area, cell_area, flags)
+
+class CellRendererToggle(CellRendererIcon):
+ __gtype_name__ = 'JournalCellRendererSelect'
+
+ def __init__(self, tree_view):
+ CellRendererIcon.__init__(self, tree_view)
+
+ self.props.width = style.GRID_CELL_SIZE
+ self.props.height = style.GRID_CELL_SIZE
+ self.props.size = style.SMALL_ICON_SIZE
+ self.props.icon_name = 'checkbox-unchecked'
+ self.props.mode = Gtk.CellRendererMode.ACTIVATABLE
+
class CellRendererDetail(CellRendererIcon):
__gtype_name__ = 'JournalCellRendererDetail'
@@ -636,6 +867,11 @@ class CellRendererActivityIcon(CellRendererIcon):
if not self._show_palette:
return None
+ # Also, if we are in batch-operations mode, return 'None'
+ from jarabe.journal.journalactivity import get_journal
+ if get_journal().is_editing_mode_present():
+ return None
+
tree_model = self.tree_view.get_model()
metadata = tree_model.get_metadata(self.props.palette_invoker.path)
diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py
index efd0dbe..877c1ab 100644
--- a/src/jarabe/journal/misc.py
+++ b/src/jarabe/journal/misc.py
@@ -312,3 +312,37 @@ def get_icon_color(metadata):
return XoColor(client.get_string('/desktop/sugar/user/color'))
else:
return XoColor(metadata['icon-color'])
+
+
+def get_xo_serial():
+ _OFW_TREE = '/ofw'
+ _PROC_TREE = '/proc/device-tree'
+ _SN = 'serial-number'
+ _not_available = _('Not available')
+
+ serial_no = None
+ if os.path.exists(os.path.join(_OFW_TREE, _SN)):
+ serial_no = read_file(os.path.join(_OFW_TREE, _SN))
+ elif os.path.exists(os.path.join(_PROC_TREE, _SN)):
+ serial_no = read_file(os.path.join(_PROC_TREE, _SN))
+
+ if serial_no is None:
+ serial_no = _not_available
+
+ # Remove the trailing binary character, else DBUS will crash.
+ return serial_no.rstrip('\x00')
+
+
+def read_file(path):
+ if os.access(path, os.R_OK) == 0:
+ return None
+
+ fd = open(path, 'r')
+ value = fd.read()
+ fd.close()
+ if value:
+ value = value.strip('\n')
+ return value
+ else:
+ logging.debug('No information in file or directory: %s', path)
+ return None
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
index 0a5b354..c9e08ec 100644
--- a/src/jarabe/journal/model.py
+++ b/src/jarabe/journal/model.py
@@ -16,6 +16,7 @@
import logging
import os
+import stat
import errno
import subprocess
from datetime import datetime
@@ -37,6 +38,8 @@ from sugar3 import dispatch
from sugar3 import mime
from sugar3 import util
+from jarabe.journal import webdavmanager
+
DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
@@ -50,14 +53,99 @@ PROPERTIES = ['activity', 'activity_id', 'buddies', 'bundle_id',
MIN_PAGES_TO_CACHE = 3
MAX_PAGES_TO_CACHE = 5
+WEBDAV_MOUNT_POINT = '/tmp/'
+LOCAL_SHARES_MOUNT_POINT = '/var/www/web1/web/'
+
JOURNAL_METADATA_DIR = '.Sugar-Metadata'
+LIST_VIEW = 1
+DETAIL_VIEW = 2
+
_datastore = None
created = dispatch.Signal()
updated = dispatch.Signal()
deleted = dispatch.Signal()
+SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME_PATH = \
+ '/desktop/sugar/network/school_server_ip_address_or_dns_name'
+IS_PEER_TO_PEER_SHARING_AVAILABLE_PATH = \
+ '/desktop/sugar/network/is_peer_to_peer_sharing_available'
+
+client = GConf.Client.get_default()
+SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME = client.get_string(SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME_PATH) or ''
+IS_PEER_TO_PEER_SHARING_AVAILABLE = client.get_bool(IS_PEER_TO_PEER_SHARING_AVAILABLE_PATH)
+
+
+
+def is_school_server_present():
+ return not (SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME is '')
+
+
+def is_peer_to_peer_sharing_available():
+ return IS_PEER_TO_PEER_SHARING_AVAILABLE == True
+
+
+def _get_mount_point(path):
+ dir_path = os.path.dirname(path)
+ while dir_path:
+ if os.path.ismount(dir_path):
+ return dir_path
+ else:
+ dir_path = dir_path.rsplit(os.sep, 1)[0]
+ return None
+
+
+def _check_remote_sharing_mount_point(mount_point, share_type):
+ from jarabe.journal.journalactivity import get_journal
+
+ mount_point_button = get_journal().get_volumes_toolbar()._get_button_for_mount_point(mount_point)
+ if mount_point_button._share_type == share_type:
+ return True
+ return False
+
+
+def is_mount_point_for_school_server(mount_point):
+ from jarabe.journal.volumestoolbar import SHARE_TYPE_SCHOOL_SERVER
+ return _check_remote_sharing_mount_point(mount_point, SHARE_TYPE_SCHOOL_SERVER)
+
+
+def is_mount_point_for_peer_share(mount_point):
+ from jarabe.journal.volumestoolbar import SHARE_TYPE_PEER
+ return _check_remote_sharing_mount_point(mount_point, SHARE_TYPE_PEER)
+
+
+def is_current_mount_point_for_remote_share(view_type):
+ from jarabe.journal.journalactivity import get_journal, get_mount_point
+ if view_type == LIST_VIEW:
+ current_mount_point = get_mount_point()
+ elif view_type == DETAIL_VIEW:
+ current_mount_point = get_journal().get_detail_toolbox().get_mount_point()
+
+ if is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ return True
+ return False
+
+
+def extract_ip_address_or_dns_name_from_locally_mounted_remote_share_path(path):
+ """
+ Path is of type ::
+
+ /tmp/1.2.3.4/webdav/a.txt; OR
+ /tmp/this.is.dns.name/a.txt
+ """
+ return path.split('/')[2]
+
+
+def is_mount_point_for_locally_mounted_remote_share(mount_point):
+ """
+ The mount-point can be either of the ip-Address, or the DNS name.
+ More importantly, whatever the "name" be, it does NOT have a
+ forward-slash.
+ """
+ return mount_point.find(WEBDAV_MOUNT_POINT) == 0
+
+
class _Cache(object):
__gtype_name__ = 'model_Cache'
@@ -422,6 +510,127 @@ class InplaceResultSet(BaseResultSet):
return
+class RemoteShareResultSet(object):
+ def __init__(self, ip_address_or_dns_name, query):
+ self._ip_address_or_dns_name = ip_address_or_dns_name
+ self._file_list = []
+
+ self.ready = dispatch.Signal()
+ self.progress = dispatch.Signal()
+
+ # First time, query is none.
+ if query is None:
+ return
+
+ query_text = query.get('query', '')
+ if query_text.startswith('"') and query_text.endswith('"'):
+ self._regex = re.compile('*%s*' % query_text.strip(['"']))
+ elif query_text:
+ expression = ''
+ for word in query_text.split(' '):
+ expression += '(?=.*%s.*)' % word
+ self._regex = re.compile(expression, re.IGNORECASE)
+ else:
+ self._regex = None
+
+ if query.get('timestamp', ''):
+ self._date_start = int(query['timestamp']['start'])
+ self._date_end = int(query['timestamp']['end'])
+ else:
+ self._date_start = None
+ self._date_end = None
+
+ self._mime_types = query.get('mime_type', [])
+
+ self._sort = query.get('order_by', ['+timestamp'])[0]
+
+ def setup(self):
+ try:
+ metadata_list_complete = webdavmanager.get_remote_webdav_share_metadata(self._ip_address_or_dns_name)
+ except Exception, e:
+ metadata_list_complete = []
+
+ for metadata in metadata_list_complete:
+
+ add_to_list = False
+ if self._regex is not None:
+ for f in ['fulltext', 'title',
+ 'description', 'tags']:
+ if f in metadata and \
+ self._regex.match(metadata[f]):
+ add_to_list = True
+ break
+ else:
+ add_to_list = True
+ if not add_to_list:
+ continue
+
+ add_to_list = False
+ if self._date_start is not None:
+ if metadata['timestamp'] > self._date_start:
+ add_to_list = True
+ else:
+ add_to_list = True
+ if not add_to_list:
+ continue
+
+ add_to_list = False
+ if self._date_end is not None:
+ if metadata['timestamp'] < self._date_end:
+ add_to_list = True
+ else:
+ add_to_list = True
+ if not add_to_list:
+ continue
+
+ add_to_list = False
+ if self._mime_types:
+ mime_type = metadata['mime_type']
+ if mime_type in self._mime_types:
+ add_to_list = True
+ else:
+ add_to_list = True
+ if not add_to_list:
+ continue
+
+ # If control reaches here, the current metadata has passed
+ # out all filter-tests.
+ file_info = (metadata['timestamp'],
+ metadata['creation_time'],
+ metadata['filesize'],
+ metadata)
+ self._file_list.append(file_info)
+
+ if self._sort[1:] == 'filesize':
+ keygetter = itemgetter(2)
+ elif self._sort[1:] == 'creation_time':
+ keygetter = itemgetter(1)
+ else:
+ # timestamp
+ keygetter = itemgetter(0)
+
+ self._file_list.sort(lambda a, b: cmp(b, a),
+ key=keygetter,
+ reverse=(self._sort[0] == '-'))
+
+ self.ready.send(self)
+
+ def get_length(self):
+ return len(self._file_list)
+
+ length = property(get_length)
+
+ def seek(self, position):
+ self._position = position
+
+ def read(self):
+ modified_timestamp, creation_timestamp, filesize, metadata = self._file_list[self._position]
+ return metadata
+
+ def stop(self):
+ self._stopped = True
+
+
def _get_file_metadata(path, stat, fetch_preview=True):
"""Return the metadata from the corresponding file.
@@ -434,9 +643,13 @@ def _get_file_metadata(path, stat, fetch_preview=True):
metadata = _get_file_metadata_from_json(dir_path, filename, fetch_preview)
if metadata:
if 'filesize' not in metadata:
- metadata['filesize'] = stat.st_size
+ if stat is not None:
+ metadata['filesize'] = stat.st_size
return metadata
+ if stat is None:
+ raise ValueError('File does not exist')
+
mime_type, uncertain_result_ = Gio.content_type_guess(filename=path,
data=None)
return {'uid': path,
@@ -457,10 +670,17 @@ def _get_file_metadata_from_json(dir_path, filename, fetch_preview):
If the metadata is corrupted we do remove it and the preview as well.
"""
+
+ # In case of nested mount-points, (eg. ~/Documents/in1/in2/in3.txt),
+ # "dir_path = ~/Documents/in1/in2"; while
+ # "metadata_dir_path = ~/Documents".
+ from jarabe.journal.journalactivity import get_mount_point
+ metadata_dir_path = get_mount_point()
+
metadata = None
- metadata_path = os.path.join(dir_path, JOURNAL_METADATA_DIR,
+ metadata_path = os.path.join(metadata_dir_path, JOURNAL_METADATA_DIR,
filename + '.metadata')
- preview_path = os.path.join(dir_path, JOURNAL_METADATA_DIR,
+ preview_path = os.path.join(metadata_dir_path, JOURNAL_METADATA_DIR,
filename + '.preview')
if not os.path.exists(metadata_path):
@@ -529,6 +749,9 @@ def find(query_, page_size):
if mount_points[0] == '/':
return DatastoreResultSet(query, page_size)
+ elif is_mount_point_for_locally_mounted_remote_share(mount_points[0]):
+ ip_address = extract_ip_address_or_dns_name_from_locally_mounted_remote_share_path(mount_points[0])
+ return RemoteShareResultSet(ip_address, query)
else:
return InplaceResultSet(query, page_size, mount_points[0])
@@ -546,8 +769,12 @@ def _get_mount_point(path):
def get(object_id):
"""Returns the metadata for an object
"""
- if os.path.exists(object_id):
- stat = os.stat(object_id)
+ if (object_id[0] == '/'):
+ if os.path.exists(object_id):
+ stat = os.stat(object_id)
+ else:
+ stat = None
+
metadata = _get_file_metadata(object_id, stat)
metadata['mountpoint'] = _get_mount_point(object_id)
else:
@@ -620,7 +847,21 @@ def delete(object_id):
def copy(metadata, mount_point):
"""Copies an object to another mount point
"""
+ # In all cases (except one), "copy" means the actual duplication of
+ # the content.
+ # Only in case of remote downloading, the content is first copied
+ # to "/tmp" folder. In those cases, copying would refer to a mere
+ # renaming.
+ transfer_ownership = False
+
+ from jarabe.journal.journalactivity import get_mount_point
+ current_mount_point = get_mount_point()
+
+ if is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ transfer_ownership = True
+
metadata = get(metadata['uid'])
+
if mount_point == '/' and metadata['icon-color'] == '#000000,#ffffff':
client = GConf.Client.get_default()
metadata['icon-color'] = client.get_string('/desktop/sugar/user/color')
@@ -631,7 +872,7 @@ def copy(metadata, mount_point):
metadata['mountpoint'] = mount_point
del metadata['uid']
- return write(metadata, file_path, transfer_ownership=False)
+ return write(metadata, file_path, transfer_ownership=transfer_ownership)
def write(metadata, file_path='', update_mtime=True, transfer_ownership=True):
@@ -653,22 +894,113 @@ def write(metadata, file_path='', update_mtime=True, transfer_ownership=True):
object_id = _get_datastore().create(dbus.Dictionary(metadata),
file_path,
transfer_ownership)
+ elif metadata.get('mountpoint', '/') == (WEBDAV_MOUNT_POINT + SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME):
+ filename = metadata['title']
+
+ ip_address_or_dns_name = SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME
+ webdavmanager.get_remote_webdav_share_metadata(ip_address_or_dns_name)
+
+ data_webdav_manager = \
+ webdavmanager.get_data_webdav_manager(ip_address_or_dns_name)
+ metadata_webdav_manager = \
+ webdavmanager.get_metadata_webdav_manager(ip_address_or_dns_name)
+
+
+ # If we get a resource by this name, there is already an entry
+ # on the server with this name; we do not want to do any
+ # overwrites.
+ data_resource = webdavmanager.get_resource_by_resource_key(data_webdav_manager,
+ '/webdav/' + filename)
+ metadata_resource = webdavmanager.get_resource_by_resource_key(metadata_webdav_manager,
+ '/webdav/.Sugar-Metadata/' + filename + '.metadata')
+ if (data_resource is not None) or (metadata_resource is not None):
+ raise Exception(_('Entry already present on the server with '
+ 'this name. Try again after renaming.'))
+
+ # No entry for this name present.
+ # So, first write the metadata- and preview-file to temporary
+ # locations.
+ metadata_file_path, preview_file_path = \
+ _write_metadata_and_preview_files_and_return_file_paths(metadata,
+ filename)
+
+ # Finally,
+ # Upload the data file.
+ webdavmanager.add_resource_by_resource_key(data_webdav_manager,
+ filename,
+ file_path)
+
+ # Upload the preview file.
+ if preview_file_path is not None:
+ webdavmanager.add_resource_by_resource_key(metadata_webdav_manager,
+ filename + '.preview',
+ preview_file_path)
+
+ # Upload the metadata file.
+ #
+ # Note that this needs to be the last step. If there was any
+ # error uploading the data- or the preview-file, control would
+ # not reach here.
+ #
+ # In other words, the control reaches here only if the data-
+ # and the preview- files have been uploaded. Finally, IF this
+ # file is successfully uploaded, we have the guarantee that all
+ # files for a particular journal entry are in place.
+ webdavmanager.add_resource_by_resource_key(metadata_webdav_manager,
+ filename + '.metadata',
+ metadata_file_path)
+
+
+ object_id = 'doesn\'t matter'
+
else:
- object_id = _write_entry_on_external_device(metadata, file_path)
+ object_id = _write_entry_on_external_device(metadata,
+ file_path,
+ transfer_ownership)
return object_id
-def _rename_entry_on_external_device(file_path, destination_path,
- metadata_dir_path):
+def make_file_fully_permissible(file_path):
+ fd = os.open(file_path, os.O_RDONLY)
+ os.fchmod(fd, stat.S_IRWXU | stat.S_IRWXG |stat.S_IRWXO)
+ os.close(fd)
+
+
+def _rename_entry_on_external_device(file_path, destination_path):
"""Rename an entry with the associated metadata on an external device."""
old_file_path = file_path
if old_file_path != destination_path:
- os.rename(file_path, destination_path)
+ # Strangely, "os.rename" works fine on sugar-jhbuild, but fails
+ # on XOs, wih the OSError 13 ("invalid cross-device link"). So,
+ # using the system call "mv".
+ os.system('mv "%s" "%s"' % (file_path, destination_path))
+ make_file_fully_permissible(destination_path)
+
+
+ # In renaming, we want to delete the metadata-, and preview-
+ # files of the current mount-point, and not the destination
+ # mount-point.
+ # But we also need to ensure that the directory of
+ # 'old_file_path' and 'destination_path' are not same.
+ if os.path.dirname(old_file_path) == os.path.dirname(destination_path):
+ return
+
+ from jarabe.journal.journalactivity import get_mount_point
+
+ # Also, as a special case, the metadata- and preview-files of
+ # the remote-shares must never be deleted. For them, only the
+ # data-file needs to be moved.
+ if is_mount_point_for_locally_mounted_remote_share(get_mount_point()):
+ return
+
+
+ source_metadata_dir_path = get_mount_point() + '/.Sugar-Metadata'
+
old_fname = os.path.basename(file_path)
- old_files = [os.path.join(metadata_dir_path,
+ old_files = [os.path.join(source_metadata_dir_path,
old_fname + '.metadata'),
- os.path.join(metadata_dir_path,
+ os.path.join(source_metadata_dir_path,
old_fname + '.preview')]
for ofile in old_files:
if os.path.exists(ofile):
@@ -679,41 +1011,32 @@ def _rename_entry_on_external_device(file_path, destination_path,
'for file=%s', ofile, old_fname)
-def _write_entry_on_external_device(metadata, file_path):
- """Create and update an entry copied from the
- DS to an external storage device.
-
- Besides copying the associated file a file for the preview
- and one for the metadata are stored in the hidden directory
- .Sugar-Metadata.
-
- This function handles renames of an entry on the
- external device and avoids name collisions. Renames are
- handled failsafe.
-
- """
- if 'uid' in metadata and os.path.exists(metadata['uid']):
- file_path = metadata['uid']
+def _write_metadata_and_preview_files_and_return_file_paths(metadata,
+ file_name):
+ metadata_copy = metadata.copy()
+ metadata_copy.pop('mountpoint', None)
+ metadata_copy.pop('uid', None)
- if not file_path or not os.path.exists(file_path):
- raise ValueError('Entries without a file cannot be copied to '
- 'removable devices')
- if not metadata.get('title'):
- metadata['title'] = _('Untitled')
- file_name = get_file_name(metadata['title'], metadata['mime_type'])
+ # For copying to School-Server, we need to retain this property.
+ # Else wise, I have no idea why this property is being removed !!
+ if (is_mount_point_for_locally_mounted_remote_share(metadata.get('mountpoint', '/')) == False) and \
+ (metadata.get('mountpoint', '/') != LOCAL_SHARES_MOUNT_POINT):
+ metadata_copy.pop('filesize', None)
- destination_path = os.path.join(metadata['mountpoint'], file_name)
- if destination_path != file_path:
- file_name = get_unique_file_name(metadata['mountpoint'], file_name)
- destination_path = os.path.join(metadata['mountpoint'], file_name)
- clean_name, extension_ = os.path.splitext(file_name)
- metadata['title'] = clean_name
+ # For journal case, there is the special treatment.
+ if metadata.get('mountpoint', '/') == '/':
+ if metadata.get('uid', ''):
+ object_id = _get_datastore().update(metadata['uid'],
+ dbus.Dictionary(metadata),
+ '',
+ False)
+ else:
+ object_id = _get_datastore().create(dbus.Dictionary(metadata),
+ '',
+ False)
+ return
- metadata_copy = metadata.copy()
- metadata_copy.pop('mountpoint', None)
- metadata_copy.pop('uid', None)
- metadata_copy.pop('filesize', None)
metadata_dir_path = os.path.join(metadata['mountpoint'],
JOURNAL_METADATA_DIR)
@@ -742,11 +1065,64 @@ def _write_entry_on_external_device(metadata, file_path):
os.close(fh)
os.rename(fn, os.path.join(metadata_dir_path, preview_fname))
- if not os.path.dirname(destination_path) == os.path.dirname(file_path):
- shutil.copy(file_path, destination_path)
+ metadata_destination_path = os.path.join(metadata_dir_path, file_name + '.metadata')
+ make_file_fully_permissible(metadata_destination_path)
+ if preview:
+ preview_destination_path = os.path.join(metadata_dir_path, preview_fname)
+ make_file_fully_permissible(preview_destination_path)
+ else:
+ preview_destination_path = None
+
+ return (metadata_destination_path, preview_destination_path)
+
+
+def update_only_metadata_and_preview_files_and_return_file_paths(metadata):
+ file_name = get_file_name(metadata['title'], metadata['mime_type'])
+ _write_metadata_and_preview_files_and_return_file_paths(metadata,
+ file_name)
+
+
+def _write_entry_on_external_device(metadata, file_path,
+ transfer_ownership):
+ """Create and update an entry copied from the
+ DS to an external storage device.
+
+ Besides copying the associated file a file for the preview
+ and one for the metadata are stored in the hidden directory
+ .Sugar-Metadata.
+
+ This function handles renames of an entry on the
+ external device and avoids name collisions. Renames are
+ handled failsafe.
+
+ """
+ if 'uid' in metadata and os.path.exists(metadata['uid']):
+ file_path = metadata['uid']
+
+ if not file_path or not os.path.exists(file_path):
+ raise ValueError('Entries without a file cannot be copied to '
+ 'removable devices')
+
+ if not metadata.get('title'):
+ metadata['title'] = _('Untitled')
+ file_name = get_file_name(metadata['title'], metadata['mime_type'])
+
+ destination_path = os.path.join(metadata['mountpoint'], file_name)
+ if destination_path != file_path:
+ file_name = get_unique_file_name(metadata['mountpoint'], file_name)
+ destination_path = os.path.join(metadata['mountpoint'], file_name)
+ clean_name, extension_ = os.path.splitext(file_name)
+ metadata['title'] = clean_name
+
+ _write_metadata_and_preview_files_and_return_file_paths(metadata,
+ file_name)
+
+ if (os.path.dirname(destination_path) == os.path.dirname(file_path)) or \
+ (transfer_ownership == True):
+ _rename_entry_on_external_device(file_path, destination_path)
else:
- _rename_entry_on_external_device(file_path, destination_path,
- metadata_dir_path)
+ shutil.copy(file_path, destination_path)
+ make_file_fully_permissible(destination_path)
object_id = destination_path
created.send(None, object_id=object_id)
@@ -796,7 +1172,17 @@ def is_editable(metadata):
if metadata.get('mountpoint', '/') == '/':
return True
else:
- return os.access(metadata['mountpoint'], os.W_OK)
+ # sl#3605: Instead of relying on mountpoint property being
+ # present in the metadata, use journalactivity api.
+ # This would work seamlessly, as "Details View' is
+ # called, upon an entry in the context of a singular
+ # mount-point.
+ from jarabe.journal.journalactivity import get_mount_point
+ mount_point = get_mount_point()
+
+ if is_mount_point_for_locally_mounted_remote_share(mount_point):
+ return False
+ return os.access(mount_point, os.W_OK)
def get_documents_path():
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
index 43f9905..f770d55 100644
--- a/src/jarabe/journal/palettes.py
+++ b/src/jarabe/journal/palettes.py
@@ -15,6 +15,7 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from gettext import gettext as _
+from gettext import ngettext
import logging
import os
@@ -38,6 +39,61 @@ from jarabe.model import mimeregistry
from jarabe.journal import misc
from jarabe.journal import model
from jarabe.journal import journalwindow
+from jarabe.journal import webdavmanager
+from jarabe.journal.journalwindow import freeze_ui, \
+ unfreeze_ui, \
+ show_normal_cursor, \
+ show_waiting_cursor
+
+from webdav.Connection import WebdavError
+
+
+friends_model = friends.get_model()
+
+_copy_menu_helper = None
+_current_action_item = None
+
+USER_FRIENDLY_GENERIC_WEBDAV_ERROR_MESSAGE = _('Cannot perform request. Connection failed.')
+
+
+class PassphraseDialog(Gtk.Dialog):
+ def __init__(self, callback, metadata):
+ Gtk.Dialog.__init__(self, flags=Gtk.DialogFlags.MODAL)
+ self._callback = callback
+ self._metadata = metadata
+ self.set_title(_('Passphrase required'))
+
+ # TRANS: Please do not translate the '%s'.
+ label_text = _('Please enter the passphrase for "%s"' % metadata['title'])
+ label = Gtk.Label(label_text)
+ self.vbox.pack_start(label, True, True, 0)
+
+ self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
+ self.set_default_response(Gtk.ResponseType.OK)
+ self.add_key_entry()
+
+ self.connect('response', self._key_dialog_response_cb)
+ self.show_all()
+
+ def add_key_entry(self):
+ self._entry = Gtk.Entry()
+ self._entry.connect('activate', self._entry_activate_cb)
+ self.vbox.pack_start(self._entry, True, True, 0)
+ self.vbox.set_spacing(6)
+ self.vbox.show_all()
+
+ self._entry.grab_focus()
+
+ def _entry_activate_cb(self, entry):
+ self.response(Gtk.ResponseType.OK)
+
+ def get_response_object(self):
+ return self._response
+
+ def _key_dialog_response_cb(self, widget, response_id):
+ self.hide()
+ GObject.idle_add(self._callback, self._metadata,
+ self._entry.get_text())
class ObjectPalette(Palette):
@@ -68,6 +124,9 @@ class ObjectPalette(Palette):
Palette.__init__(self, primary_text=title,
icon=activity_icon)
+ from jarabe.journal.journalactivity import get_mount_point
+ current_mount_point = get_mount_point()
+
if misc.get_activities(metadata) or misc.is_bundle(metadata):
if metadata.get('activity_id', ''):
resume_label = _('Resume')
@@ -77,10 +136,14 @@ class ObjectPalette(Palette):
resume_with_label = _('Start with')
menu_item = MenuItem(resume_label, 'activity-start')
menu_item.connect('activate', self.__start_activate_cb)
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ menu_item.set_sensitive(False)
self.menu.append(menu_item)
menu_item.show()
menu_item = MenuItem(resume_with_label, 'activity-start')
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ menu_item.set_sensitive(False)
self.menu.append(menu_item)
menu_item.show()
start_with_menu = StartWithMenu(self._metadata)
@@ -99,6 +162,15 @@ class ObjectPalette(Palette):
self.menu.append(menu_item)
menu_item.show()
copy_menu = CopyMenu(metadata)
+ copy_menu_helper = get_copy_menu_helper()
+
+ metadata_list = []
+ metadata_list.append(metadata)
+ copy_menu_helper.insert_copy_to_menu_items(copy_menu,
+ metadata_list,
+ False,
+ False,
+ False)
copy_menu.connect('volume-error', self.__volume_error_cb)
menu_item.set_submenu(copy_menu)
@@ -112,6 +184,8 @@ class ObjectPalette(Palette):
menu_item.show()
menu_item = MenuItem(_('Send to'), 'document-send')
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ menu_item.set_sensitive(False)
self.menu.append(menu_item)
menu_item.show()
@@ -127,6 +201,8 @@ class ObjectPalette(Palette):
menu_item = MenuItem(_('Erase'), 'list-remove')
menu_item.connect('activate', self.__erase_activate_cb)
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ menu_item.set_sensitive(False)
self.menu.append(menu_item)
menu_item.show()
@@ -197,123 +273,798 @@ class CopyMenu(Gtk.Menu):
__gsignals__ = {
'volume-error': (GObject.SignalFlags.RUN_FIRST, None,
- ([str, str])),
+ ([str, str])),
}
def __init__(self, metadata):
Gtk.Menu.__init__(self)
- self._metadata = metadata
- clipboard_menu = ClipboardMenu(self._metadata)
- clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
- icon_size=Gtk.IconSize.MENU))
- clipboard_menu.connect('volume-error', self.__volume_error_cb)
- self.append(clipboard_menu)
- clipboard_menu.show()
+class ActionItem(GObject.GObject):
+ """
+ This class implements the course of actions that happens when clicking
+ upon an Action-Item (eg. Batch-Copy-Toolbar-button;
+ Actual-Batch-Copy-To-Journal-button;
+ Actual-Batch-Copy-To-Documents-button;
+ Actual-Batch-Copy-To-Mounted-Drive-button;
+ Actual-Batch-Copy-To-Clipboard-button;
+ Single-Copy-To-Journal-button;
+ Single-Copy-To-Documents-button;
+ Single-Copy-To-Mounted-Drive-button;
+ Single-Copy-To-Clipboard-button;
+ Batch-Erase-Button;
+ Select-None-Toolbar-button;
+ Select-All-Toolbar-button
+ """
+ __gtype_name__ = 'JournalActionItem'
+
+ def __init__(self, label, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode,
+ auto_deselect_source_entries,
+ need_to_popup_options,
+ operate_on_deselected_entries,
+ show_not_completed_ops_info):
+ GObject.GObject.__init__(self)
+
+ self._label = label
+
+ # Make a copy.
+ self._immutable_metadata_list = []
+ for metadata in metadata_list:
+ self._immutable_metadata_list.append(metadata)
+
+ self._metadata_list = metadata_list
+ self._show_progress_info_alert = show_progress_info_alert
+ self._batch_mode = batch_mode
+ self._auto_deselect_source_entries = \
+ auto_deselect_source_entries
+ self._need_to_popup_options = \
+ need_to_popup_options
+ self._operate_on_deselected_entries = \
+ operate_on_deselected_entries
+ self._show_not_completed_ops_info = \
+ show_not_completed_ops_info
+
+ actionable_signal = self._get_actionable_signal()
+
+ if need_to_popup_options:
+ self.connect(actionable_signal, self._pre_fill_and_pop_up_options)
+ else:
+ if show_editing_alert:
+ self.connect(actionable_signal, self._show_editing_alert)
+ else:
+ self.connect(actionable_signal,
+ self._pre_operate_per_action,
+ Gtk.ResponseType.OK)
+
+ def _get_actionable_signal(self):
+ """
+ Some widgets like 'buttons' have 'clicked' as actionable signal;
+ some like 'menuitems' have 'activate' as actionable signal.
+ """
+
+ raise NotImplementedError
+
+ def _pre_fill_and_pop_up_options(self, widget_clicked):
+ self._set_current_action_item_widget()
+ self._fill_and_pop_up_options(widget_clicked)
+
+ def _fill_and_pop_up_options(self, widget_clicked):
+ """
+ Eg. Batch-Copy-Toolbar-button does not do anything by itself
+ useful; but rather pops-up the actual 'copy-to' options.
+ """
+
+ raise NotImplementedError
+
+ def _show_editing_alert(self, widget_clicked):
+ """
+ Upon clicking the actual operation button (eg.
+ Batch-Erase-Button and Batch-Copy-To-Clipboard button; BUT NOT
+ Batch-Copy-Toolbar-button, since it does not do anything
+ actually useful, but only pops-up the actual 'copy-to' options.
+ """
+
+ freeze_ui()
+ GObject.idle_add(self.__show_editing_alert_after_freezing_ui,
+ widget_clicked)
+
+ def __show_editing_alert_after_freezing_ui(self, widget_clicked):
+ self._set_current_action_item_widget()
+
+ alert_parameters = self._get_editing_alert_parameters()
+ title = alert_parameters[0]
+ message = alert_parameters[1]
+ operation = alert_parameters[2]
+
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_confirmation_alert(title, message,
+ self._pre_operate_per_action,
+ None)
+
+ def _get_editing_alert_parameters(self):
+ """
+ Get the alert parameters for widgets that can show editing
+ alert.
+ """
+
+ self._metadata_list = self._get_metadata_list()
+ entries_len = len(self._metadata_list)
+
+ title = self._get_editing_alert_title()
+ message = self._get_editing_alert_message(entries_len)
+ operation = self._get_editing_alert_operation()
+
+ return (title, message, operation)
+
+ def _get_list_model_len(self):
+ """
+ Get the total length of the model under view.
+ """
+
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ return len(journal.get_list_view().get_model())
+
+ def _get_metadata_list(self):
+ """
+ For batch-mode, get the metadata list, according to button-type.
+ For eg, Select-All-Toolbar-button operates on non-selected entries;
+ while othere operate on selected-entries.
+
+ For single-mode, simply copy from the
+ "immutable_metadata_list".
+ """
+
+ if self._batch_mode:
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ if self._operate_on_deselected_entries:
+ metadata_list = journal.get_metadata_list(False)
+ else:
+ metadata_list = journal.get_metadata_list(True)
- if self._metadata['mountpoint'] != '/':
- client = GConf.Client.get_default()
- color = XoColor(client.get_string('/desktop/sugar/user/color'))
- journal_menu = VolumeMenu(self._metadata, _('Journal'), '/')
- journal_menu.set_image(Icon(icon_name='activity-journal',
- xo_color=color,
- icon_size=Gtk.IconSize.MENU))
- journal_menu.connect('volume-error', self.__volume_error_cb)
- self.append(journal_menu)
- journal_menu.show()
+ # Make a backup copy, of this metadata_list.
+ self._immutable_metadata_list = []
+ for metadata in metadata_list:
+ self._immutable_metadata_list.append(metadata)
- volume_monitor = Gio.VolumeMonitor.get()
- icon_theme = Gtk.IconTheme.get_default()
- for mount in volume_monitor.get_mounts():
- if self._metadata['mountpoint'] == mount.get_root().get_path():
- continue
- volume_menu = VolumeMenu(self._metadata, mount.get_name(),
- mount.get_root().get_path())
- for name in mount.get_icon().props.names:
- if icon_theme.has_icon(name):
- volume_menu.set_image(Icon(icon_name=name,
- icon_size=Gtk.IconSize.MENU))
- break
- volume_menu.connect('volume-error', self.__volume_error_cb)
- self.append(volume_menu)
- volume_menu.show()
+ return metadata_list
+ else:
+ metadata_list = []
+ for metadata in self._immutable_metadata_list:
+ metadata_list.append(metadata)
+ return metadata_list
+
+ def _get_editing_alert_title(self):
+ raise NotImplementedError
+
+ def _get_editing_alert_message(self, entries_len):
+ raise NotImplementedError
+
+ def _get_editing_alert_operation(self):
+ raise NotImplementedError
+
+ def _is_metadata_list_empty(self):
+ return (self._metadata_list is None) or \
+ (len(self._metadata_list) == 0)
+
+ def _set_current_action_item_widget(self):
+ """
+ Only set this, if this widget achieves some effective action.
+ """
+ if not self._need_to_popup_options:
+ global _current_action_item
+ _current_action_item = self
+
+ def _pre_operate_per_action(self, obj, response_id):
+ """
+ This is the stage, just before the FIRST metadata gets into its
+ processing cycle.
+ """
+ freeze_ui()
+ GObject.idle_add(self._pre_operate_per_action_after_done_ui_freezing,
+ obj, response_id)
+
+ def _pre_operate_per_action_after_done_ui_freezing(self, obj,
+ response_id):
+ self._set_current_action_item_widget()
+
+ self._continue_operation = True
+
+ # If the user chose to cancel the operation from the onset,
+ # simply proceeed to the last.
+ if response_id == Gtk.ResponseType.CANCEL:
+ unfreeze_ui()
+
+ self._cancel_further_batch_operation_items()
+ self._post_operate_per_action()
+ return
- def __volume_error_cb(self, menu_item, message, severity):
- self.emit('volume-error', message, severity)
+ self._skip_all = False
+ # Also, get the initial length of the model.
+ self._model_len = self._get_list_model_len()
-class VolumeMenu(MenuItem):
- __gtype_name__ = 'JournalVolumeMenu'
+ # Speed Optimisation:
+ # ===================
+ # If the metadata-list is empty, fetch it;
+ # else we have already fetched it, when we showed the
+ # "editing-alert".
+ if len(self._metadata_list) == 0:
+ self._metadata_list = self._get_metadata_list()
- __gsignals__ = {
- 'volume-error': (GObject.SignalFlags.RUN_FIRST, None,
- ([str, str])),
- }
+ # Set the initial length of metadata-list.
+ self._metadata_list_initial_len = len(self._metadata_list)
- def __init__(self, metadata, label, mount_point):
- MenuItem.__init__(self, label)
- self._metadata = metadata
- self.connect('activate', self.__copy_to_volume_cb, mount_point)
+ self._metadata_processed = 0
- def __copy_to_volume_cb(self, menu_item, mount_point):
- file_path = model.get_file(self._metadata['uid'])
+ # Next, proceed with the metadata
+ self._pre_operate_per_metadata_per_action()
+
+ def _pre_operate_per_metadata_per_action(self):
+ """
+ This is the stage, just before EVERY metadata gets into doing
+ its actual work.
+ """
+
+ show_waiting_cursor()
+ GObject.idle_add(self.__pre_operate_per_metadata_per_action_after_freezing_ui)
+
+ def __pre_operate_per_metadata_per_action_after_freezing_ui(self):
+ from jarabe.journal.journalactivity import get_journal
+
+ # If there is still some metadata left, proceed with the
+ # metadata operation.
+ # Else, proceed to post-operations.
+ if len(self._metadata_list) > 0:
+ metadata = self._metadata_list.pop(0)
+
+ # If info-alert needs to be shown, show the alert, and
+ # arrange for actual operation.
+ # Else, proceed to actual operation directly.
+ if self._show_progress_info_alert:
+ current_len = len(self._metadata_list)
+
+ # TRANS: Do not translate the two %d, and the %s.
+ info_alert_message = _(' %d of %d : %s') % (
+ self._metadata_list_initial_len - current_len,
+ self._metadata_list_initial_len, metadata['title'])
+
+ get_journal().update_info_alert(self._get_info_alert_title(),
+ info_alert_message)
+
+ # Call the core-function !!
+ GObject.idle_add(self._operate_per_metadata_per_action, metadata)
+ else:
+ self._post_operate_per_action()
+
+ def _get_info_alert_title(self):
+ raise NotImplementedError
+
+ def _operate_per_metadata_per_action(self, metadata):
+ """
+ This is just a code-convenient-function, which allows
+ runtime-overriding. It just delegates to the actual
+ "self._operate" method, the actual which is determined at
+ runtime.
+ """
+
+ if self._continue_operation is False:
+ # Jump directly to the post-operation
+ self._post_operate_per_metadata_per_action(metadata)
+ else:
+ # Pass the callback for the post-operation-for-metadata. This
+ # will ensure that async-operations on the metadata are taken
+ # care of.
+ if self._operate(metadata) is False:
+ return
+ else:
+ self._metadata_processed = self._metadata_processed + 1
+
+ def _operate(self, metadata):
+ """
+ Actual, core, productive stage for EVERY metadata.
+ """
+ raise NotImplementedError
+
+ def _post_operate_per_metadata_per_action(self, metadata,
+ response_id=None):
+ """
+ This is the stage, just after EVERY metadata has been
+ processed.
+ """
+ self._hide_info_widget_for_single_mode()
+
+ # Toggle the corresponding checkbox - but only for batch-mode.
+ if self._batch_mode and self._auto_deselect_source_entries:
+ from jarabe.journal.journalactivity import get_journal
+ list_view = get_journal().get_list_view()
+
+ list_view.do_ui_select_change(metadata)
+ list_view.do_backend_select_change(metadata)
+
+ # Call the next ...
+ self._pre_operate_per_metadata_per_action()
+
+ def _post_operate_per_action(self):
+ """
+ This is the stage, just after the LAST metadata has been
+ processed.
+ """
+
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+ journal_toolbar_box = journal.get_toolbar_box()
+
+ if self._batch_mode and (not self._auto_deselect_source_entries):
+ journal_toolbar_box.display_already_selected_entries_status()
+
+ self._process_switching_mode(None, False)
+
+ unfreeze_ui()
+
+ # Set the "_current_action_item" to None.
+ global _current_action_item
+ _current_action_item = None
+
+ def _process_switching_mode(self, metadata, ok_clicked=False):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ # Necessary to do this, when the alert needs to be hidden,
+ # WITHOUT user-intervention.
+ journal.hide_alert()
+
+ def _refresh(self):
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().get_list_view().refresh()
+
+ def _handle_single_mode_notification(self, message, severity):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ journal._show_alert(message, severity)
+ self._hide_info_widget_for_single_mode()
+
+ def _hide_info_widget_for_single_mode(self):
+ if (not self._batch_mode):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ journal.get_toolbar_box().hide_info_widget()
+
+ def _unhide_info_widget_for_single_mode(self):
+ if not self._batch_mode:
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_progress(0)
+
+ def _handle_error_alert(self, error_message, metadata):
+ """
+ This handles any error scenarios. Examples are of entries that
+ display the message "Entries without a file cannot be copied."
+ This is kind of controller-functionl the model-function is
+ "self._set_error_info_alert".
+ """
+
+ if self._skip_all:
+ self._post_operate_per_metadata_per_action(metadata)
+ else:
+ self._set_error_info_alert(error_message, metadata)
+
+ def _set_error_info_alert(self, error_message, metadata):
+ """
+ This method displays the error alert.
+ """
+
+ current_len = len(self._metadata_list)
+
+ # Only show the alert, if allowed to.
+ if self._show_not_completed_ops_info:
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_confirmation_alert(_('Error'),
+ error_message,
+ self._process_error_skipping,
+ metadata)
+ else:
+ self._process_error_skipping(metadata, gtk.RESPONSE_OK)
+
+ def _process_error_skipping(self, metadata, response_id):
+ # This sets up the decision, as to whether continue operations
+ # with the rest of the metadata.
+ if response_id == Gtk.ResponseType.CANCEL:
+ self._cancel_further_batch_operation_items()
+
+ self._post_operate_per_metadata_per_action(metadata)
+
+ def _cancel_further_batch_operation_items(self):
+ self._continue_operation = False
+
+ # Optimization:
+ # Clear the metadata-list as well.
+ # This would prevent the unnecessary traversing of the
+ # remaining checkboxes-corresponding-to-remaining-metadata (of
+ # course without doing any effective action).
+ self._metadata_list = []
+
+ def _file_path_valid(self, metadata):
+ from jarabe.journal.journalactivity import get_mount_point
+ current_mount_point = get_mount_point()
+
+ # Now, for locally mounted remote-shares, download the file.
+ # Note that, always download the file, to avoid the problems
+ # of stale-cache.
+ if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+ file_path = metadata['uid']
+ filename = os.path.basename(file_path)
+ ip_address_or_dns_name = \
+ model.extract_ip_address_or_dns_name_from_locally_mounted_remote_share_path(file_path)
+
+ data_webdav_manager = \
+ webdavmanager.get_data_webdav_manager(ip_address_or_dns_name)
+ metadata_webdav_manager = \
+ webdavmanager.get_metadata_webdav_manager(ip_address_or_dns_name)
+
+ # Download the preview file, if it exists.
+ preview_resource = \
+ webdavmanager.get_resource_by_resource_key(metadata_webdav_manager,
+ '/webdav/.Sugar-Metadata/' + filename + '.preview')
+ preview_path = os.path.dirname(file_path) + '/.Sugar-Metadata/'+ filename + '.preview'
+
+ if preview_resource is not None:
+ try:
+ preview_resource.downloadFile(preview_path,
+ show_progress=False,
+ filesize=0)
+ except (WebdavError, socket.error), e:
+ error_message = USER_FRIENDLY_GENERIC_WEBDAV_ERROR_MESSAGE
+ logging.warn(error_message)
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(error_message,
+ _('Error'))
+ return False
+
+ # If we manage to reach here, download the data file.
+ data_resource = \
+ webdavmanager.get_resource_by_resource_key(data_webdav_manager,
+ '/webdav/'+ filename)
+ try:
+ data_resource.downloadFile(metadata['uid'],
+ show_progress=True,
+ filesize=int(metadata['filesize']))
+ return True
+ except (WebdavError, socket.error), e:
+ # Delete the downloaded preview file, if it exists.
+ if os.path.exists(preview_path):
+ os.unlink(preview_path)
+
+ error_message = USER_FRIENDLY_GENERIC_WEBDAV_ERROR_MESSAGE
+ logging.warn(error_message)
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(error_message,
+ _('Error'))
+ return False
+
+ file_path = model.get_file(metadata['uid'])
if not file_path or not os.path.exists(file_path):
logging.warn('Entries without a file cannot be copied.')
- self.emit('volume-error',
- _('Entries without a file cannot be copied.'),
- _('Warning'))
- return
+ error_message = _('Entries without a file cannot be copied.')
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(error_message, _('Warning'))
+ return False
+ else:
+ return True
+
+ def _metadata_copy_valid(self, metadata, mount_point):
+ self._set_bundle_installation_allowed(False)
try:
- model.copy(self._metadata, mount_point)
- except IOError, e:
- logging.exception('Error while copying the entry. %s', e.strerror)
- self.emit('volume-error',
- _('Error while copying the entry. %s') % e.strerror,
- _('Error'))
+ model.copy(metadata, mount_point)
+ return True
+ except Exception, e:
+ logging.exception(e)
+ error_message = _('Error while copying the entry. %s') % e
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(error_message, _('Error'))
+ return False
+ finally:
+ self._set_bundle_installation_allowed(True)
+ def _metadata_write_valid(self, metadata):
+ operation = self._get_info_alert_title()
+ self._set_bundle_installation_allowed(False)
-class ClipboardMenu(MenuItem):
- __gtype_name__ = 'JournalClipboardMenu'
+ try:
+ model.update_only_metadata_and_preview_files_and_return_file_paths(metadata)
+ return True
+ except Exception, e:
+ logging.exception('Error while writing the metadata. %s', e)
+ error_message = _('Error occurred while %s : %s.') % \
+ (operation, e,)
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(error_message, _('Error'))
+ return False
+ finally:
+ self._set_bundle_installation_allowed(True)
- __gsignals__ = {
- 'volume-error': (GObject.SignalFlags.RUN_FIRST, None,
- ([str, str])),
- }
+ def _set_bundle_installation_allowed(self, allowed):
+ """
+ This method serves only as a "delegating" method.
+ This has been done to aid easy configurability.
+ """
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
- def __init__(self, metadata):
- MenuItem.__init__(self, _('Clipboard'))
+ if self._batch_mode:
+ journal.set_bundle_installation_allowed(allowed)
- self._temp_file_path = None
- self._metadata = metadata
- self.connect('activate', self.__copy_to_clipboard_cb)
+ def get_number_of_entries_to_operate_upon(self):
+ return len(self._immutable_metadata_list)
- def __copy_to_clipboard_cb(self, menu_item):
- file_path = model.get_file(self._metadata['uid'])
- if not file_path or not os.path.exists(file_path):
- logging.warn('Entries without a file cannot be copied.')
- self.emit('volume-error',
- _('Entries without a file cannot be copied.'),
- _('Warning'))
- return
+
+class BaseCopyMenuItem(MenuItem, ActionItem):
+ __gtype_name__ = 'JournalBaseCopyMenuItem'
+
+ __gsignals__ = {
+ 'volume-error': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str, str])),
+ }
+
+ def __init__(self, metadata_list, label, show_editing_alert,
+ show_progress_info_alert, batch_mode, mount_point):
+ MenuItem.__init__(self, label)
+ ActionItem.__init__(self, label, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode,
+ auto_deselect_source_entries=False,
+ need_to_popup_options=False,
+ operate_on_deselected_entries=False,
+ show_not_completed_ops_info=True)
+ self._mount_point = mount_point
+
+ def get_mount_point(self):
+ return self._mount_point
+
+ def _get_actionable_signal(self):
+ return 'activate'
+
+ def _get_editing_alert_title(self):
+ return _('Copy')
+
+ def _get_editing_alert_message(self, entries_len):
+ return ngettext('Do you want to copy %d entry to %s?',
+ 'Do you want to copy %d entries to %s?',
+ entries_len) % (entries_len, self._label)
+
+ def _get_editing_alert_operation(self):
+ return _('Copy')
+
+ def _get_info_alert_title(self):
+ return _('Copying')
+
+ def _operate(self, metadata):
+ from jarabe.journal.journalactivity import get_mount_point
+ if(model.is_mount_point_for_locally_mounted_remote_share(get_mount_point())) \
+ and (model.is_mount_point_for_school_server(get_mount_point()) == True):
+ PassphraseDialog(self._proceed_after_receiving_passphrase, metadata)
+ else:
+ self._proceed_with_copy(metadata)
+
+ def _proceed_after_receiving_passphrase(self, metadata, passphrase):
+ if metadata['passphrase'] != passphrase:
+ error_message = _('Passphrase does not match.')
+ if self._batch_mode:
+ self._handle_error_alert(error_message, metadata)
+ else:
+ self._handle_single_mode_notification(error_message, _('Error'))
+ return False
+ else:
+ self._unhide_info_widget_for_single_mode()
+ GObject.idle_add(self._proceed_with_copy, metadata)
+
+ def _proceed_with_copy(self, metadata):
+ return NotImplementedError
+
+ def _post_successful_copy(self, metadata, response_id=None):
+ from jarabe.journal.journalactivity import get_journal, \
+ get_mount_point
+
+ if model.is_mount_point_for_locally_mounted_remote_share(get_mount_point()):
+ successful_downloading_message = None
+
+ if model.is_mount_point_for_school_server(get_mount_point()) == True:
+ # TRANS: Do not translate the %s.
+ successful_downloading_message = \
+ _('Your file "%s" was correctly downloaded from the School Server.' % metadata['title'])
+ else:
+ # TRANS: Do not translate the %s.
+ successful_downloading_message = \
+ _('Your file "%s" was correctly downloaded from the Peer.' % metadata['title'])
+
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().update_error_alert(self._get_editing_alert_title(),
+ successful_downloading_message,
+ self._post_operate_per_metadata_per_action,
+ metadata)
+ else:
+ self._post_operate_per_metadata_per_action(metadata)
+
+
+class VolumeMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, label, mount_point,
+ show_editing_alert, show_progress_info_alert,
+ batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, label,
+ show_editing_alert,
+ show_progress_info_alert, batch_mode,
+ mount_point)
+
+ def _proceed_with_copy(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+
+ if not self._metadata_copy_valid(metadata, self._mount_point):
+ return False
+
+ # This is sync-operation. Thus, call the callback.
+ self._post_successful_copy(metadata)
+
+
+class ClipboardMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('Clipboard'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode, None)
+ self._temp_file_path_list = []
+
+ def _proceed_with_copy(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
clipboard = Gtk.Clipboard()
clipboard.set_with_data([('text/uri-list', 0, 0)],
self.__clipboard_get_func_cb,
- self.__clipboard_clear_func_cb)
+ self.__clipboard_clear_func_cb,
+ metadata)
- def __clipboard_get_func_cb(self, clipboard, selection_data, info, data):
+ def __clipboard_get_func_cb(self, clipboard, selection_data, info,
+ metadata):
# Get hold of a reference so the temp file doesn't get deleted
- self._temp_file_path = model.get_file(self._metadata['uid'])
+ self._temp_file_path = model.get_file(metadata['uid'])
logging.debug('__clipboard_get_func_cb %r', self._temp_file_path)
selection_data.set_uris(['file://' + self._temp_file_path])
- def __clipboard_clear_func_cb(self, clipboard, data):
+ def __clipboard_clear_func_cb(self, clipboard, metadata):
# Release and delete the temp file
self._temp_file_path = None
+ # This is async-operation; and this is the ending point.
+ self._post_successful_copy(metadata)
+
+
+class DocumentsMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('Documents'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode,
+ model.get_documents_path())
+
+ def _proceed_with_copy(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+
+ if not self._metadata_copy_valid(metadata,
+ model.get_documents_path()):
+ return False
+
+ # This is sync-operation. Call the post-operation now.
+ self._post_successful_copy(metadata)
+
+
+class LocalSharesMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('Local Shares'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode,
+ model.LOCAL_SHARES_MOUNT_POINT)
+
+ def _proceed_with_copy(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+
+ # Attach the filesize.
+ file_path = model.get_file(metadata['uid'])
+ metadata['filesize'] = os.stat(file_path).st_size
+
+ # Attach the current mount-point.
+ from jarabe.journal.journalactivity import get_mount_point
+ metadata['mountpoint'] = get_mount_point()
+
+ if not self._metadata_write_valid(metadata):
+ return False
+
+ if not self._metadata_copy_valid(metadata,
+ model.LOCAL_SHARES_MOUNT_POINT):
+ return False
+
+ # This is sync-operation. Call the post-operation now.
+ self._post_successful_copy(metadata)
+
+
+class SchoolServerMenu(BaseCopyMenuItem):
+ def __init__(self, metadata_list, show_editing_alert,
+ show_progress_info_alert, batch_mode):
+ BaseCopyMenuItem.__init__(self, metadata_list, _('School Server'),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode,
+ model.WEBDAV_MOUNT_POINT + model.SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME)
+
+ def _operate(self, metadata):
+ if not self._file_path_valid(metadata):
+ return False
+
+ # If the entry is copyable, proceed with asking the
+ # upload-passphrase.
+ PassphraseDialog(self._proceed_after_receiving_passphrase, metadata)
+
+ def _proceed_after_receiving_passphrase(self, metadata, passphrase):
+ self._unhide_info_widget_for_single_mode()
+ GObject.idle_add(self._proceed_with_uploading, metadata,
+ passphrase)
+
+ def _proceed_with_uploading(self, metadata, passphrase):
+ #
+ # Attach the passphrase.
+ metadata['passphrase'] = passphrase
+
+ # Attach the filesize.
+ file_path = model.get_file(metadata['uid'])
+ metadata['filesize'] = os.stat(file_path).st_size
+
+ # Attach the current mount-point.
+ from jarabe.journal.journalactivity import get_mount_point, \
+ get_journal
+ metadata['mountpoint'] = get_mount_point()
+
+ # Attach the info of the uploader.
+ from jarabe.model.buddy import get_owner_instance
+ metadata['uploader-nick'] = get_owner_instance().props.nick
+ metadata['uploader-serial'] = misc.get_xo_serial()
+
+ if not self._metadata_write_valid(metadata):
+ return False
+
+ if not self._metadata_copy_valid(metadata,
+ model.WEBDAV_MOUNT_POINT + model.SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME):
+ return False
+
+ # TRANS: Do not translate the %s.
+ successful_uploading_message = \
+ _('Your file "%s" was correctly uploaded to the School Server.' % metadata['title'])
+ get_journal().update_error_alert(self._get_editing_alert_title(),
+ successful_uploading_message,
+ self._post_successful_copy,
+ metadata)
+
class FriendsMenu(Gtk.Menu):
__gtype_name__ = 'JournalFriendsMenu'
@@ -401,3 +1152,116 @@ class BuddyPalette(Palette):
icon=buddy_icon)
# TODO: Support actions on buddies, like make friend, invite, etc.
+
+
+class CopyMenuHelper(Gtk.Menu):
+ __gtype_name__ = 'JournalCopyMenuHelper'
+
+ __gsignals__ = {
+ 'volume-error': (GObject.SignalFlags.RUN_FIRST,
+ None, ([str, str])),
+ }
+
+ def insert_copy_to_menu_items(self, menu, metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode):
+ self._metadata_list = metadata_list
+
+ clipboard_menu = ClipboardMenu(metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ clipboard_menu.set_image(Icon(icon_name='toolbar-edit',
+ icon_size=Gtk.IconSize.MENU))
+ clipboard_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(clipboard_menu)
+ clipboard_menu.show()
+
+ from jarabe.journal.journalactivity import get_mount_point
+
+ if get_mount_point() != model.get_documents_path():
+ documents_menu = DocumentsMenu(metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ documents_menu.set_image(Icon(icon_name='user-documents',
+ icon_size=Gtk.IconSize.MENU))
+ documents_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(documents_menu)
+ documents_menu.show()
+
+ if (model.is_school_server_present()) and \
+ (not model.is_mount_point_for_locally_mounted_remote_share(get_mount_point())):
+ documents_menu = SchoolServerMenu(metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ documents_menu.set_image(Icon(icon_name='school-server',
+ icon_size=Gtk.IconSize.MENU))
+ documents_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(documents_menu)
+ documents_menu.show()
+
+ if (model.is_peer_to_peer_sharing_available()) and \
+ (get_mount_point() != model.LOCAL_SHARES_MOUNT_POINT):
+ local_shares_menu = LocalSharesMenu(metadata_list,
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ local_shares_menu.set_image(Icon(icon_name='emblem-neighborhood-shared',
+ icon_size=Gtk.IconSize.MENU))
+ local_shares_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(local_shares_menu)
+ local_shares_menu.show()
+
+ if get_mount_point() != '/':
+ client = GConf.Client.get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ journal_menu = VolumeMenu(metadata_list, _('Journal'), '/',
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ journal_menu.set_image(Icon(icon_name='activity-journal',
+ xo_color=color,
+ icon_size=Gtk.IconSize.MENU))
+ journal_menu.connect('volume-error', self.__volume_error_cb)
+ menu.append(journal_menu)
+ journal_menu.show()
+
+ volume_monitor = Gio.VolumeMonitor.get()
+ icon_theme = Gtk.IconTheme.get_default()
+ for mount in volume_monitor.get_mounts():
+ if get_mount_point() == mount.get_root().get_path():
+ continue
+
+ volume_menu = VolumeMenu(metadata_list, mount.get_name(),
+ mount.get_root().get_path(),
+ show_editing_alert,
+ show_progress_info_alert,
+ batch_mode)
+ for name in mount.get_icon().props.names:
+ if icon_theme.has_icon(name):
+ volume_menu.set_image(Icon(icon_name=name,
+ icon_size=Gtk.IconSize.MENU))
+ break
+
+ volume_menu.connect('volume-error', self.__volume_error_cb)
+ menu.insert(volume_menu, -1)
+ volume_menu.show()
+
+ def __volume_error_cb(self, menu_item, message, severity):
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+ journal._volume_error_cb(menu_item, message, severity)
+
+
+def get_copy_menu_helper():
+ global _copy_menu_helper
+ if _copy_menu_helper is None:
+ _copy_menu_helper = CopyMenuHelper()
+ return _copy_menu_helper
+
+
+def get_current_action_item():
+ return _current_action_item
diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py
index 1fc368e..ed2a0a3 100644
--- a/src/jarabe/journal/volumestoolbar.py
+++ b/src/jarabe/journal/volumestoolbar.py
@@ -37,11 +37,14 @@ from sugar3.graphics.xocolor import XoColor
from sugar3 import env
from jarabe.journal import model
-from jarabe.view.palettes import VolumePalette
+from jarabe.view.palettes import VolumePalette, RemoteSharePalette
_JOURNAL_0_METADATA_DIR = '.olpc.store'
+SHARE_TYPE_PEER = 1
+SHARE_TYPE_SCHOOL_SERVER = 2
+
def _get_id(document):
"""Get the ID for the document in the xapian database."""
@@ -193,6 +196,17 @@ class VolumesToolbar(Gtk.Toolbar):
def _set_up_volumes(self):
self._set_up_documents_button()
+ if model.is_peer_to_peer_sharing_available():
+ self._set_up_local_shares_button()
+
+ client = GConf.Client.get_default()
+ color = XoColor(client.get_string('/desktop/sugar/user/color'))
+
+ if model.is_school_server_present():
+ self._add_remote_share_button(_('School-Server Shares'),
+ model.SCHOOL_SERVER_IP_ADDRESS_OR_DNS_NAME,
+ color, SHARE_TYPE_SCHOOL_SERVER)
+
volume_monitor = Gio.VolumeMonitor.get()
self._mount_added_hid = volume_monitor.connect('mount-added',
self.__mount_added_cb)
@@ -202,12 +216,11 @@ class VolumesToolbar(Gtk.Toolbar):
for mount in volume_monitor.get_mounts():
self._add_button(mount)
- def _set_up_documents_button(self):
- documents_path = model.get_documents_path()
- if documents_path is not None:
- button = DocumentsButton(documents_path)
+ def _set_up_directory_button(self, dir_path, icon_name, label_text):
+ if dir_path is not None:
+ button = DirectoryButton(dir_path, icon_name)
button.props.group = self._volume_buttons[0]
- label = glib.markup_escape_text(_('Documents'))
+ label = glib.markup_escape_text(label_text)
button.set_palette(Palette(label))
button.connect('toggled', self._button_toggled_cb)
button.show()
@@ -217,6 +230,44 @@ class VolumesToolbar(Gtk.Toolbar):
self._volume_buttons.append(button)
self.show()
+ def _set_up_documents_button(self):
+ documents_path = model.get_documents_path()
+ self._set_up_directory_button(documents_path,
+ 'user-documents',
+ _('Documents'))
+
+ def _set_up_local_shares_button(self):
+ local_shares_path = model.LOCAL_SHARES_MOUNT_POINT
+ self._set_up_directory_button(local_shares_path,
+ 'emblem-neighborhood-shared',
+ _('Local Shares'))
+
+ def _add_remote_share_button(self, primary_text,
+ ip_address_or_dns_name, color,
+ share_type):
+ button = RemoteSharesButton(primary_text, ip_address_or_dns_name,
+ color, share_type)
+ button._share_type = share_type
+ button.props.group = self._volume_buttons[0]
+
+ show_unmount_option = None
+ if share_type == SHARE_TYPE_PEER:
+ show_unmount_option = True
+ else:
+ show_unmount_option = False
+ button.set_palette(RemoteSharePalette(primary_text,
+ ip_address_or_dns_name, button,
+ show_unmount_option))
+ button.connect('toggled', self._button_toggled_cb)
+ button.show()
+
+ position = self.get_item_index(self._volume_buttons[-1]) + 1
+ self.insert(button, position)
+ self._volume_buttons.append(button)
+ self.show()
+
+ return button
+
def __mount_added_cb(self, volume_monitor, mount):
self._add_button(mount)
@@ -247,10 +298,26 @@ class VolumesToolbar(Gtk.Toolbar):
def __volume_error_cb(self, button, strerror, severity):
self.emit('volume-error', strerror, severity)
- def _button_toggled_cb(self, button):
- if button.props.active:
+ def _button_toggled_cb(self, button, force_toggle=False):
+ if button.props.active or force_toggle:
+ button.set_active(True)
+ from jarabe.journal.journalactivity import get_journal
+ journal = get_journal()
+
+ journal.hide_alert()
+ journal.get_list_view()._selected_entries = 0
+ journal.switch_to_editing_mode(False)
+ journal.get_list_view().inhibit_refresh(False)
+
self.emit('volume-changed', button.mount_point)
+ def _unmount_activated_cb(self, menu_item, mount):
+ logging.debug('VolumesToolbar._unmount_activated_cb: %r', mount)
+ mount.unmount(self.__unmount_cb)
+
+ def __unmount_cb(self, source, result):
+ logging.debug('__unmount_cb %r %r', source, result)
+
def _get_button_for_mount(self, mount):
mount_point = mount.get_root().get_path()
for button in self.get_children():
@@ -259,6 +326,13 @@ class VolumesToolbar(Gtk.Toolbar):
logging.error('Couldnt find button with mount_point %r', mount_point)
return None
+ def _get_button_for_mount_point(self, mount_point):
+ for button in self.get_children():
+ if button.mount_point == mount_point:
+ return button
+ logging.error('Couldnt find button with mount_point %r', mount_point)
+ return None
+
def _remove_button(self, mount):
button = self._get_button_for_mount(mount)
self._volume_buttons.remove(button)
@@ -268,10 +342,33 @@ class VolumesToolbar(Gtk.Toolbar):
if len(self.get_children()) < 2:
self.hide()
+ def _remove_remote_share_button(self, ip_address_or_dns_name):
+ for button in self.get_children():
+ if type(button) == RemoteSharesButton and \
+ button.mount_point == (model.WEBDAV_MOUNT_POINT + ip_address_or_dns_name):
+ self._volume_buttons.remove(button)
+ self.remove(button)
+
+ from jarabe.journal.webdavmanager import \
+ unmount_share_from_backend
+ unmount_share_from_backend(ip_address_or_dns_name)
+
+ self.get_children()[0].props.active = True
+
+ if len(self.get_children()) < 2:
+ self.hide()
+ break;
+
def set_active_volume(self, mount):
button = self._get_button_for_mount(mount)
button.props.active = True
+ def get_journal_button(self):
+ return self._volume_buttons[0]
+
+ def get_button_toggled_cb(self):
+ return self._button_toggled_cb
+
class BaseButton(RadioToolButton):
__gsignals__ = {
@@ -291,23 +388,36 @@ class BaseButton(RadioToolButton):
def _drag_data_received_cb(self, widget, drag_context, x, y,
selection_data, info, timestamp):
+ # Disallow copying to mounted-shares for peers.
+ if (model.is_mount_point_for_locally_mounted_remote_share(self.mount_point)) and \
+ (model.is_mount_point_for_peer_share(self.mount_point)):
+ from jarabe.journal.journalactivity import get_journal
+
+ journal = get_journal()
+ journal._show_alert(_('Entries cannot be copied to Peer-Shares.'), _('Error'))
+ return
+
object_id = selection_data.data
metadata = model.get(object_id)
- file_path = model.get_file(metadata['uid'])
- if not file_path or not os.path.exists(file_path):
- logging.warn('Entries without a file cannot be copied.')
- self.emit('volume-error',
- _('Entries without a file cannot be copied.'),
- _('Warning'))
- return
- try:
- model.copy(metadata, self.mount_point)
- except IOError, e:
- logging.exception('Error while copying the entry. %s', e.strerror)
- self.emit('volume-error',
- _('Error while copying the entry. %s') % e.strerror,
- _('Error'))
+ from jarabe.journal.palettes import CopyMenu, get_copy_menu_helper
+ copy_menu_helper = get_copy_menu_helper()
+
+ dummy_copy_menu = CopyMenu()
+ copy_menu_helper.insert_copy_to_menu_items(dummy_copy_menu,
+ [metadata],
+ False,
+ False,
+ False)
+
+ # Now, activate the menuitem, whose mount-point matches the
+ # mount-point of the button, upon whom the item has been
+ # dragged.
+ children_menu_items = dummy_copy_menu.get_children()
+ for child in children_menu_items:
+ if child.get_mount_point() == self.mount_point:
+ child.activate()
+ return
class VolumeButton(BaseButton):
@@ -386,13 +496,35 @@ class JournalButtonPalette(Palette):
{'free_space': free_space / (1024 * 1024)}
-class DocumentsButton(BaseButton):
+class DirectoryButton(BaseButton):
- def __init__(self, documents_path):
- BaseButton.__init__(self, mount_point=documents_path)
+ def __init__(self, dir_path, icon_name):
+ BaseButton.__init__(self, mount_point=dir_path)
+ self._mount = dir_path
- self.props.icon_name = 'user-documents'
+ self.props.icon_name = icon_name
client = GConf.Client.get_default()
color = XoColor(client.get_string('/desktop/sugar/user/color'))
self.props.xo_color = color
+
+
+class RemoteSharesButton(BaseButton):
+
+ def __init__(self, primary_text, ip_address_or_dns_name, color,
+ share_type):
+ BaseButton.__init__(self, mount_point=(model.WEBDAV_MOUNT_POINT + ip_address_or_dns_name))
+
+ self._primary_text = primary_text
+ self._ip_address_or_dns_name = ip_address_or_dns_name
+
+ if share_type == SHARE_TYPE_PEER:
+ self.props.icon_name = 'emblem-neighborhood-shared'
+ elif share_type == SHARE_TYPE_SCHOOL_SERVER:
+ self.props.icon_name = 'school-server'
+ self.props.xo_color = color
+
+ def create_palette(self):
+ palette = RemoteSharePalette(self._primary_text, self._ip_address_or_dns_name,
+ self, True)
+ return palette
diff --git a/src/jarabe/journal/webdavmanager.py b/src/jarabe/journal/webdavmanager.py
new file mode 100644
index 0000000..6936239
--- /dev/null
+++ b/src/jarabe/journal/webdavmanager.py
@@ -0,0 +1,312 @@
+from gettext import gettext as _
+
+from gi.repository import GObject
+
+import logging
+import os
+import sys
+
+import simplejson
+import shutil
+
+from webdav.Connection import AuthorizationError, WebdavError
+from webdav.WebdavClient import CollectionStorer
+
+def get_key_from_resource(resource):
+ return resource.path
+
+class WebDavUrlManager(GObject.GObject):
+ """
+ This class holds all data, relevant to a WebDavUrl.
+
+ One thing must be noted, that a valid WebDavUrl is the one which
+ may contain zero or more resources (files), or zero or more
+ collections (directories).
+
+ Thus, following are valid WebDavUrls ::
+
+ dav://1.2.3.4/webdav
+ dav://1.2.3.4/webdav/dir_1
+ dav://1.2.3.4/webdav/dir_1/dir_2
+
+ but following are not ::
+
+ dav://1.2.3.4/webdav/a.txt
+ dav://1.2.3.4/webdav/dir_1/b.jpg
+ dav://1.2.3.4/webdab/dir_1/dir_2/c.avi
+ """
+
+ def __init__(self, WebDavUrl, username, password):
+ self._WebDavUrl = WebDavUrl
+ self._username = username
+ self._password = password
+
+ def _get_key_from_resource(self, resource):
+ return resource.path.encode(sys.getfilesystemencoding())
+
+ def _get_number_of_collections(self):
+ return len(self._remote_webdav_share_collections)
+
+ def _get_root(self):
+ return self._root
+
+ def _get_resources_dict(self):
+ return self._remote_webdav_share_resources
+
+ def _get_collections_dict(self):
+ return self._remote_webdav_share_collections
+
+ def _get_resource_by_key(self, key):
+ if key in self._remote_webdav_share_resources.keys():
+ return self._remote_webdav_share_resources[key]['resource']
+ return None
+
+ def _add_or_replace_resource_by_key(self, key, resource):
+ self._remote_webdav_share_resources[key] = {}
+ self._remote_webdav_share_resources[key]['resource'] = resource
+
+ def _get_metadata_list(self):
+ metadata_list = []
+ for key in self._remote_webdav_share_resources.keys():
+ metadata_list.append(self._remote_webdav_share_resources[key]['metadata'])
+ return metadata_list
+
+ def _get_live_properties(self, resource_key):
+ resource_container = self._remote_webdav_share_resources[resource_key]
+ return resource_container['webdav-properties']
+
+ def _fetch_resources_and_collections(self):
+ webdavConnection = CollectionStorer(self._WebDavUrl, validateResourceNames=False)
+ self._root = webdavConnection
+
+ authFailures = 0
+ while authFailures < 2:
+ try:
+ self._remote_webdav_share_resources = {}
+ self._remote_webdav_share_collections = {}
+
+ try:
+ self._collection_contents = webdavConnection.getCollectionContents()
+ for resource, properties in self._collection_contents:
+ try:
+ key = self._get_key_from_resource(resource)
+ selected_dict = None
+
+ if properties.getResourceType() == 'resource':
+ selected_dict = self._remote_webdav_share_resources
+ else:
+ selected_dict = self._remote_webdav_share_collections
+
+ selected_dict[key] = {}
+ selected_dict[key]['resource'] = resource
+ selected_dict[key]['webdav-properties'] = properties
+ except UnicodeEncodeError:
+ print("Cannot encode resource path or properties.")
+
+ return True
+
+ except WebdavError, e:
+ # Note that, we need to deal with all errors,
+ # except "AuthorizationError", as that is not
+ # really an error from our perspective.
+ if not type(e) == AuthorizationError:
+ from jarabe.journal.journalactivity import get_journal
+
+ from jarabe.journal.palettes import USER_FRIENDLY_GENERIC_WEBDAV_ERROR_MESSAGE
+ error_message = USER_FRIENDLY_GENERIC_WEBDAV_ERROR_MESSAGE
+ get_journal()._volume_error_cb(None, error_message,_('Error'))
+
+ # Re-raise this error.
+ # Note that since this is not an
+ # "AuthorizationError", this will not be caught
+ # by the outer except-block. Instead, it will
+ # navigate all the way back up, and will report
+ # the error in the enclosing except block.
+ raise e
+
+ else:
+ # If this indeed is an Authorization Error,
+ # re-raise it, so that it is caught by the outer
+ # "except" block.
+ raise e
+
+
+ except AuthorizationError, e:
+ if self._username is None or self._password is None:
+ raise Exception("WebDav username or password is None. Please specify appropriate values.")
+
+ if e.authType == "Basic":
+ webdavConnection.connection.addBasicAuthorization(self._username, self._password)
+ elif e.authType == "Digest":
+ info = parseDigestAuthInfo(e.authInfo)
+ webdavConnection.connection.addDigestAuthorization(self._username, self._password, realm=info["realm"], qop=info["qop"], nonce=info["nonce"])
+ else:
+ raise
+ authFailures += 1
+
+ return False
+
+webdav_manager = {}
+
+def get_data_webdav_manager(ip_address_or_dns_name):
+ return webdav_manager[ip_address_or_dns_name]['data']
+
+
+def get_metadata_webdav_manager(ip_address_or_dns_name):
+ return webdav_manager[ip_address_or_dns_name]['metadata']
+
+
+def get_resource_by_resource_key(root_webdav, key):
+ resources_dict = root_webdav._get_resources_dict()
+ if key in resources_dict.keys():
+ resource_dict = resources_dict[key]
+ resource = resource_dict['resource']
+ return resource
+ return None
+
+
+def add_resource_by_resource_key(root_webdav, key,
+ content_file_path):
+ root = root_webdav._get_root()
+
+ resource = root.addResource(key)
+
+ # Procure the resource-lock.
+ lockToken = resource.lock('olpc')
+
+ input_stream = open(content_file_path)
+
+ # Now, upload the data; but it's necessary to enclose this in a
+ # try-except-finally block here, since we need to close the
+ # input-stream, whatever may happen.
+ try:
+ resource.uploadFile(input_stream, lockToken)
+ root_webdav._add_or_replace_resource_by_key(key, resource)
+ except Exception, e:
+ logging.exception(e)
+ resource.delete(lockToken)
+ raise e
+ else:
+ resource.unlock(lockToken)
+ finally:
+ input_stream.close()
+
+
+def get_remote_webdav_share_metadata(ip_address_or_dns_name):
+ protocol = 'davs://'
+ root_webdav_url = '/webdav'
+ complete_root_url = protocol + ip_address_or_dns_name + root_webdav_url
+
+ root_webdav = WebDavUrlManager(complete_root_url, 'test', 'olpc')
+ if root_webdav._fetch_resources_and_collections() is False:
+ # Return empty metadata list.
+ return []
+
+ # Keep reference to the "WebDavUrlManager", keyed by IP-Address.
+ global webdav_manager
+ webdav_manager[ip_address_or_dns_name] = {}
+ webdav_manager[ip_address_or_dns_name]['data'] = root_webdav
+
+
+ # Assert that the number of collections is only one at this url
+ # (i.e. only ".Sugar-Metadata" is present).
+ assert root_webdav._get_number_of_collections() == 1
+
+ root_sugar_metadata_url = root_webdav_url + '/.Sugar-Metadata'
+
+ complete_root_sugar_metadata_url = protocol + ip_address_or_dns_name + root_sugar_metadata_url
+ root_webdav_sugar_metadata = WebDavUrlManager(complete_root_sugar_metadata_url, 'test', 'olpc')
+ if root_webdav_sugar_metadata._fetch_resources_and_collections() is False:
+ # Return empty metadata list.
+ return []
+
+ webdav_manager[ip_address_or_dns_name]['metadata'] = root_webdav_sugar_metadata
+
+ # assert that the number of collections is zero at this url.
+ assert root_webdav_sugar_metadata._get_number_of_collections() == 0
+
+ # Now. associate sugar-metadata with each of the "root-webdav"
+ # resource.
+ root_webdav_resources = root_webdav._get_resources_dict()
+ root_webdav_sugar_metadata_resources = root_webdav_sugar_metadata._get_resources_dict()
+
+ # Prepare the metadata-download folder.
+ downloaded_data_root_dir = '/tmp/' + ip_address_or_dns_name
+ downloaded_metadata_file_dir = downloaded_data_root_dir + '/.Sugar-Metadata'
+ if os.path.isdir(downloaded_data_root_dir):
+ shutil.rmtree(downloaded_data_root_dir)
+ os.makedirs(downloaded_metadata_file_dir)
+
+ metadata_list = []
+
+ # Note that the presence of a resource in the metadata directory,
+ # is the only assurance of the entry (and its constituents) being
+ # present in entirety. Thus, always proceed taking the metadata as
+ # the "key".
+ for root_webdav_sugar_metadata_resource_name in root_webdav_sugar_metadata_resources.keys():
+ """
+ root_webdav_sugar_metadata_resource_name is of the type ::
+
+ /webdav/.Sugar-Metadata/a.txt.metadata, OR
+ /webdav/.Sugar-Metadata/a.txt.preview
+ """
+
+ # If this is a "preview" resource, continue forward, as we only
+ # want the metadata list. The "preview" resources are anyways
+ # already present in the manager DS.
+ if root_webdav_sugar_metadata_resource_name.endswith('.preview'):
+ continue
+
+ split_tokens_array = root_webdav_sugar_metadata_resource_name.split('/')
+
+ # This will provide us with "a.txt.metadata"
+ sugar_metadata_basename = split_tokens_array[len(split_tokens_array) - 1]
+
+ # This will provide us with "a.txt"
+ basename = sugar_metadata_basename[0:sugar_metadata_basename.index('.metadata')]
+
+ downloaded_metadata_file_path = downloaded_metadata_file_dir + '/' + sugar_metadata_basename
+ metadata_resource = \
+ root_webdav_sugar_metadata._get_resource_by_key(root_webdav_sugar_metadata_resource_name)
+ metadata_resource.downloadFile(downloaded_metadata_file_path)
+
+
+ # We need to download the preview-file as well at this stage,
+ # so that it can be shown in the expanded entry.
+ downloaded_preview_file_path = downloaded_metadata_file_dir + \
+ '/' + basename + '.preview'
+ root_webdav_sugar_preview_resource_name = \
+ root_webdav_sugar_metadata_resource_name[0:root_webdav_sugar_metadata_resource_name.index('.metadata')] + \
+ '.preview'
+ preview_resource = \
+ root_webdav_sugar_metadata._get_resource_by_key(root_webdav_sugar_preview_resource_name)
+ if preview_resource is not None:
+ preview_resource.downloadFile(downloaded_preview_file_path)
+
+
+ file_pointer = open(downloaded_metadata_file_path)
+ metadata = eval(file_pointer.read())
+ file_pointer.close()
+
+ # Fill in the missing metadata properties.
+ # Note that the file is not physically present.
+ metadata['uid'] = downloaded_data_root_dir + '/' + basename
+ metadata['creation_time'] = metadata['timestamp']
+
+ # Now, write this to the metadata-file, so that
+ # webdav-properties get gelled into sugar-metadata.
+ file_pointer = open(downloaded_metadata_file_path, 'w')
+ file_pointer.write(simplejson.dumps(metadata))
+ file_pointer.close()
+
+ metadata_list.append(metadata)
+
+ return metadata_list
+
+
+def is_remote_webdav_loaded(ip_address_or_dns_name):
+ return ip_address_or_dns_name in webdav_manager.keys()
+
+
+def unmount_share_from_backend(ip_address_or_dns_name):
+ del webdav_manager[ip_address_or_dns_name]
diff --git a/src/jarabe/model/neighborhood.py b/src/jarabe/model/neighborhood.py
index 85c35c9..bf9cbf3 100644
--- a/src/jarabe/model/neighborhood.py
+++ b/src/jarabe/model/neighborhood.py
@@ -929,6 +929,9 @@ class Neighborhood(GObject.GObject):
if 'key' in properties:
buddy.props.key = properties['key']
+ if 'ip4-address' in properties:
+ buddy.props.ip_address = properties['ip4-address']
+
nick_key = CONNECTION_INTERFACE_ALIASING + '/alias'
if nick_key in properties:
buddy.props.nick = properties[nick_key]
diff --git a/src/jarabe/view/buddymenu.py b/src/jarabe/view/buddymenu.py
index d17f4ff..93790fd 100644
--- a/src/jarabe/view/buddymenu.py
+++ b/src/jarabe/view/buddymenu.py
@@ -73,6 +73,18 @@ class BuddyMenu(Palette):
self.menu.append(menu_item)
menu_item.show()
+ remote_share_menu_item = None
+ from jarabe.journal import webdavmanager
+ if not webdavmanager.is_remote_webdav_loaded(self._buddy.props.ip_address):
+ remote_share_menu_item = MenuItem(_('Access Share'), 'list-add')
+ remote_share_menu_item.connect('activate', self._access_share_cb)
+ else:
+ remote_share_menu_item = MenuItem(_('Unmount Share'), 'list-remove')
+ remote_share_menu_item.connect('activate', self.__unmount_cb)
+
+ self.menu.append(remote_share_menu_item)
+ remote_share_menu_item.show()
+
self._invite_menu = MenuItem('')
self._invite_menu.connect('activate', self._invite_friend_cb)
self.menu.append(self._invite_menu)
diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py
index 10844ea..4ccf8bc 100644
--- a/src/jarabe/view/palettes.py
+++ b/src/jarabe/view/palettes.py
@@ -268,3 +268,54 @@ class VolumePalette(Palette):
self._progress_bar.props.fraction = fraction
self._free_space_label.props.label = _('%(free_space)d MB Free') % \
{'free_space': free_space / (1024 * 1024)}
+
+
+class RemoteSharePalette(Palette):
+ def __init__(self, primary_text, ip_address_or_dns_name, button,
+ show_unmount_option):
+ Palette.__init__(self, label=primary_text)
+ self._button = button
+ self._ip_address_or_dns_name = ip_address_or_dns_name
+
+ self.props.secondary_text = \
+ glib.markup_escape_text(self._ip_address_or_dns_name)
+
+ vbox = Gtk.VBox()
+ self.set_content(vbox)
+ vbox.show()
+
+ self.connect('popup', self.__popup_cb)
+
+ menu_item = PaletteMenuItem(pgettext('Share', _('Reload')))
+ icon = Icon(icon_name='system-restart', icon_size=Gtk.IconSize.MENU)
+ menu_item.set_image(icon)
+ icon.show()
+
+ menu_item.connect('activate', self.__reload_remote_share)
+ vbox.add(menu_item)
+ menu_item.show()
+
+
+ if show_unmount_option == True:
+ menu_item = PaletteMenuItem(pgettext('Share', 'Unmount'))
+ icon = Icon(icon_name='media-eject', icon_size=gtk.ICON_SIZE_MENU)
+ menu_item.set_image(icon)
+ icon.show()
+
+ menu_item.connect('activate', self.__unmount_activate_cb)
+ vbox.add(menu_item)
+ menu_item.show()
+
+ def __reload_remote_share(self, menu_item):
+ from jarabe.journal.journalactivity import get_journal
+ get_journal().hide_alert()
+ get_journal().get_list_view().refresh()
+
+ def __unmount_activate_cb(self, menu_item):
+ from jarabe.journal.journalactivity import get_journal
+
+ singleton_volumes_toolbar = get_journal().get_volumes_toolbar()
+ singleton_volumes_toolbar._remove_remote_share_button(self._ip_address_or_dns_name)
+
+ def __popup_cb(self, palette):
+ pass