diff options
author | Walter Bender <walter.bender@gmail.com> | 2013-04-03 03:50:36 (GMT) |
---|---|---|
committer | Walter Bender <walter.bender@gmail.com> | 2013-04-03 03:50:36 (GMT) |
commit | d0110fcdeb9c0c5d83e3730215410ba720e21702 (patch) | |
tree | e9a42bbaddb25fa588a47bb9bf2fe193e661a12f | |
parent | 567769dc3139caf39ab1d4cac057f77255d1d2e3 (diff) |
cleanup based on feedback from dnarvaez
-rw-r--r-- | extensions/web/facebook/Makefile.am | 2 | ||||
-rw-r--r-- | extensions/web/facebook/facebookaccount.py | 281 | ||||
-rw-r--r-- | extensions/web/twitter/Makefile.am | 2 | ||||
-rw-r--r-- | extensions/web/twitter/twitteraccount.py | 283 | ||||
-rw-r--r-- | src/jarabe/journal/journaltoolbox.py | 19 | ||||
-rw-r--r-- | src/jarabe/journal/palettes.py | 4 | ||||
-rw-r--r-- | src/jarabe/web/Makefile.am | 6 | ||||
-rw-r--r-- | src/jarabe/web/__init__.py | 18 | ||||
-rw-r--r-- | src/jarabe/web/account.py | 78 | ||||
-rw-r--r-- | src/jarabe/web/accountsmanager.py | 85 |
10 files changed, 744 insertions, 34 deletions
diff --git a/extensions/web/facebook/Makefile.am b/extensions/web/facebook/Makefile.am index e2e6d1f..f9bac63 100644 --- a/extensions/web/facebook/Makefile.am +++ b/extensions/web/facebook/Makefile.am @@ -3,4 +3,4 @@ SUBDIRS = facebook icons sugardir = $(pkgdatadir)/extensions/web/facebook sugar_PYTHON = \ __init__.py \ - facebook_online_account.py + facebookaccount.py diff --git a/extensions/web/facebook/facebookaccount.py b/extensions/web/facebook/facebookaccount.py new file mode 100644 index 0000000..f105fdb --- /dev/null +++ b/extensions/web/facebook/facebookaccount.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python +# +# Copyright (c) 2013 Walter Bender, Raul Gutierrez Segales + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in +#all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +#THE SOFTWARE. + +from gettext import gettext as _ +import logging +import os +import tempfile +import time +import json + +from gi.repository import Gtk +from gi.repository import GdkPixbuf +from gi.repository import GConf +from gi.repository import GObject + +from sugar3.datastore import datastore +from sugar3.graphics.alert import NotifyAlert +from sugar3.graphics.icon import Icon + +from jarabe.journal import journalwindow +from jarabe.web import account + +from facebook import facebook + +ACCOUNT_NEEDS_ATTENTION = 0 +ACCOUNT_ACTIVE = 1 +ACCOUNT_NAME = _('Facebook') +COMMENTS = 'comments' +COMMENT_IDS = 'fb_comment_ids' + +class FacebookAccount(account.Account): + + ACCESS_TOKEN_KEY = "/desktop/sugar/collaboration/facebook_access_token" + ACCESS_TOKEN_KEY_EXPIRATION_DATE = \ + "/desktop/sugar/collaboration/facebook_access_token_expiration_date" + + def __init__(self): + self._client = GConf.Client.get_default() + facebook.FbAccount.set_access_token(self._access_token()) + self._alert = None + + def get_description(self): + return ACCOUNT_NAME + + def is_configured(self): + return self._access_token() is not None + + def is_active(self): + expiration_date = \ + self._client.get_int(self.ACCESS_TOKEN_KEY_EXPIRATION_DATE) + return expiration_date != 0 and expiration_date > time.time() + + def get_share_menu(self, journal_entry_metadata): + fb_share_menu = _FacebookShareMenu(journal_entry_metadata, + self.is_active()) + self._connect_transfer_signals(fb_share_menu) + return fb_share_menu + + def get_refresh_menu(self): + fb_refresh_menu = _FacebookRefreshMenu(self.is_active()) + self._connect_transfer_signals(fb_refresh_menu) + return fb_refresh_menu + + def _connect_transfer_signals(self, transfer_widget): + transfer_widget.connect('transfer-state-changed', + self._transfer_state_changed_cb) + + def _transfer_state_changed_cb(self, widget, state_message): + logging.debug('_transfer_state_changed_cb') + + # First, remove any existing alert + if self._alert is None: + logging.debug('creating new alert') + self._alert = NotifyAlert() + self._alert.props.title = ACCOUNT_NAME + self._alert.connect('response', self._alert_response_cb) + journalwindow.get_journal_window().add_alert(self._alert) + self._alert.show() + + logging.debug(state_message) + self._alert.props.msg = state_message + + def _alert_response_cb(self, alert, response_id): + journalwindow.get_journal_window().remove_alert(alert) + self._alert = None + + def _access_token(self): + return self._client.get_string(self.ACCESS_TOKEN_KEY) + + +class _FacebookShareMenu(account.MenuItem): + __gtype_name__ = 'JournalFacebookMenu' + + def __init__(self, metadata, is_active): + account.MenuItem.__init__(self, ACCOUNT_NAME) + + if is_active: + icon_name = 'facebook-share' + else: + icon_name = 'facebook-share-insensitive' + self.set_image(Icon(icon_name=icon_name, + icon_size=Gtk.IconSize.MENU)) + self.show() + self._metadata = metadata + self._comment = '%s: %s' % (self._get_metadata_by_key('title'), + self._get_metadata_by_key('description')) + + self.connect('activate', self._facebook_share_menu_cb) + + def _get_metadata_by_key(self, key, default_value=''): + if key in self._metadata: + return self._metadata[key] + return default_value + + def _facebook_share_menu_cb(self, menu_item): + logging.debug('_facebook_share_menu_cb') + + self.emit('transfer-state-changed', _('Upload started')) + tmp_file = tempfile.mktemp() + self._image_file_from_metadata(tmp_file) + + photo = facebook.FbPhoto() + photo.connect('photo-created', self._photo_created_cb, tmp_file) + photo.connect('photo-create-failed', + self._photo_create_failed_cb, + tmp_file) + photo.connect('transfer-state-changed', + self._transfer_state_changed_cb) + + GObject.idle_add(photo.create, tmp_file) + + def _photo_created_cb(self, fb_photo, fb_object_id, tmp_file): + logging.debug("_photo_created_cb") + + if os.path.exists(tmp_file): + os.unlink(tmp_file) + + fb_photo.connect('comment-added', self._comment_added_cb) + fb_photo.connect('comment-add-failed', self._comment_add_failed_cb) + fb_photo.add_comment(self._comment) + + try: + ds_object = datastore.get(self._metadata['uid']) + ds_object.metadata['fb_object_id'] = fb_object_id + datastore.write(ds_object, update_mtime=False) + except Exception as ex: + logging.debug("_photo_created_cb failed to write to datastore: " % \ + str(ex)) + + def _photo_create_failed_cb(self, fb_photo, failed_reason, tmp_file): + logging.debug("_photo_create_failed_cb") + + if os.path.exists(tmp_file): + os.unlink(tmp_file) + + def _comment_added_cb(self, fb_photo, fb_comment_id): + logging.debug("_comment_added_cb") + + def _comment_add_failed_cb(self, fb_photo, failed_reason): + logging.debug("_comment_add_failed_cb") + + def _image_file_from_metadata(self, image_path): + """ Load a pixbuf from a Journal object. """ + pixbufloader = \ + GdkPixbuf.PixbufLoader.new_with_mime_type('image/png') + pixbufloader.set_size(300, 225) + try: + pixbufloader.write(self._metadata['preview']) + pixbuf = pixbufloader.get_pixbuf() + except Exception as ex: + logging.debug("_image_file_from_metadata: %s" % (str(ex))) + pixbuf = None + + pixbufloader.close() + if pixbuf: + pixbuf.savev(image_path, 'png', [], []) + + +class _FacebookRefreshMenu(account.MenuItem): + def __init__(self, is_active): + account.MenuItem.__init__(self, ACCOUNT_NAME) + + self._is_active = is_active + self._metadata = None + + if is_active: + icon_name = 'facebook-refresh' + else: + icon_name = 'facebook-refresh-insensitive' + self.set_image(Icon(icon_name=icon_name, + icon_size=Gtk.IconSize.MENU)) + self.show() + + self.connect('activate', self._fb_refresh_menu_clicked_cb) + + def set_metadata(self, metadata): + self._metadata = metadata + if self._is_active: + if self._metadata: + if 'fb_object_id' in self._metadata: + self.set_sensitive(True) + icon_name = 'facebook-refresh' + else: + self.set_sensitive(False) + icon_name = 'facebook-refresh-insensitive' + self.set_image(Icon(icon_name=icon_name, + icon_size=Gtk.IconSize.MENU)) + + def _fb_refresh_menu_clicked_cb(self, button): + logging.debug('_fb_refresh_menu_clicked_cb') + + if self._metadata is None: + logging.debug('_fb_refresh_menu_clicked_cb called without metadata') + return + + if 'fb_object_id' not in self._metadata: + logging.debug('_fb_refresh_menu_clicked_cb called without fb_object_id in metadata') + return + + self.emit('transfer-state-changed', _('Download started')) + fb_photo = facebook.FbPhoto(self._metadata['fb_object_id']) + fb_photo.connect('comments-downloaded', + self._fb_comments_downloaded_cb) + fb_photo.connect('comments-download-failed', + self._fb_comments_download_failed_cb) + fb_photo.connect('transfer-state-changed', + self._transfer_state_changed_cb) + GObject.idle_add(fb_photo.refresh_comments) + + def _fb_comments_downloaded_cb(self, fb_photo, comments): + logging.debug('_fb_comments_downloaded_cb') + + ds_object = datastore.get(self._metadata['uid']) + if not COMMENTS in ds_object.metadata: + ds_comments = [] + else: + ds_comments = json.loads(ds_object.metadata[COMMENTS]) + if not COMMENT_IDS in ds_object.metadata: + ds_comment_ids = [] + else: + ds_comment_ids = json.loads(ds_object.metadata[COMMENT_IDS]) + new_comment = False + for comment in comments: + if comment['id'] not in ds_comment_ids: + # TODO: get avatar icon and add it to icon_theme + ds_comments.append({'from': comment['from'], + 'message': comment['message'], + 'icon': 'facebook-share'}) + ds_comment_ids.append(comment['id']) + new_comment = True + if new_comment: + ds_object.metadata[COMMENTS] = json.dumps(ds_comments) + ds_object.metadata[COMMENT_IDS] = json.dumps(ds_comment_ids) + self.emit('comments-changed', ds_object.metadata[COMMENTS]) + + datastore.write(ds_object, update_mtime=False) + + def _fb_comments_download_failed_cb(self, fb_photo, failed_reason): + logging.debug('_fb_comments_download_failed_cb: %s' % (failed_reason)) + +def get_account(): + return FacebookAccount() diff --git a/extensions/web/twitter/Makefile.am b/extensions/web/twitter/Makefile.am index 8346b41..438d3fa 100644 --- a/extensions/web/twitter/Makefile.am +++ b/extensions/web/twitter/Makefile.am @@ -3,4 +3,4 @@ SUBDIRS = twitter icons sugardir = $(pkgdatadir)/extensions/web/twitter sugar_PYTHON = \ __init__.py \ - twitter_online_account.py + twitteraccount.py diff --git a/extensions/web/twitter/twitteraccount.py b/extensions/web/twitter/twitteraccount.py new file mode 100644 index 0000000..a2e4bf0 --- /dev/null +++ b/extensions/web/twitter/twitteraccount.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python +# +# Copyright (c) 2013 Walter Bender, Raul Gutierrez Segales, Martin Abente + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in +#all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +#THE SOFTWARE. + +from gettext import gettext as _ +import logging +import os +import tempfile +import time +import json + +from gi.repository import Gtk +from gi.repository import GdkPixbuf +from gi.repository import GConf +from gi.repository import GObject + +from sugar3.datastore import datastore +from sugar3.graphics.alert import NotifyAlert +from sugar3.graphics.icon import Icon + +from jarabe.journal import journalwindow +from jarabe.web import account + +from twitter.twr_account import TwrAccount +from twitter.twr_status import TwrStatus +from twitter.twr_timeline import TwrTimeline + +ACCOUNT_NEEDS_ATTENTION = 0 +ACCOUNT_ACTIVE = 1 +ACCOUNT_NAME = _('Twitter') +COMMENTS = 'comments' +COMMENT_IDS = 'twr_comment_ids' +COMMENT_LAST_ID = 'last_comment_id' + + +class TwitterAccount(account.Account): + + CONSUMER_TOKEN_KEY = "/desktop/sugar/collaboration/twitter_consumer_token" + CONSUMER_SECRET_KEY = "/desktop/sugar/collaboration/twitter_consumer_secret" + ACCESS_TOKEN_KEY = "/desktop/sugar/collaboration/twitter_access_token" + ACCESS_SECRET_KEY = "/desktop/sugar/collaboration/twitter_access_secret" + + def __init__(self): + self._client = GConf.Client.get_default() + ctoken, csecret, atoken, asecret = self._access_tokens() + TwrAccount.set_secrets(ctoken, csecret, atoken, asecret) + self._alert = None + + def get_description(self): + return ACCOUNT_NAME + + def is_configured(self): + return None not in self._access_tokens() + + def is_active(self): + # No expiration date + return None not in self._access_tokens() + + def get_share_menu(self, journal_entry_metadata): + twr_share_menu = _TwitterShareMenu(journal_entry_metadata, + self.is_active()) + self._connect_transfer_signals(twr_share_menu) + return twr_share_menu + + def get_refresh_menu(self): + twr_refresh_menu = _TwitterRefreshMenu(self.is_active()) + self._connect_transfer_signals(twr_refresh_menu) + return twr_refresh_menu + + def _connect_transfer_signals(self, transfer_widget): + transfer_widget.connect('transfer-state-changed', + self._transfer_state_changed_cb) + + def _transfer_state_changed_cb(self, widget, state_message): + logging.debug('_transfer_state_changed_cb') + + # First, remove any existing alert + if self._alert is None: + self._alert = NotifyAlert() + self._alert.props.title = ACCOUNT_NAME + self._alert.connect('response', self._alert_response_cb) + journalwindow.get_journal_window().add_alert(self._alert) + self._alert.show() + + logging.debug(state_message) + self._alert.props.msg = state_message + + def _alert_response_cb(self, alert, response_id): + journalwindow.get_journal_window().remove_alert(alert) + self._alert = None + + def _access_tokens(self): + return (self._client.get_string(self.CONSUMER_TOKEN_KEY), + self._client.get_string(self.CONSUMER_SECRET_KEY), + self._client.get_string(self.ACCESS_TOKEN_KEY), + self._client.get_string(self.ACCESS_SECRET_KEY)) + + +class _TwitterShareMenu(account.MenuItem): + __gtype_name__ = 'JournalTwitterMenu' + + def __init__(self, metadata, is_active): + account.MenuItem.__init__(self, ACCOUNT_NAME) + + if is_active: + icon_name = 'twitter-share' + else: + icon_name = 'twitter-share-insensitive' + self.set_image(Icon(icon_name=icon_name, + icon_size=Gtk.IconSize.MENU)) + self.show() + self._metadata = metadata + self._comment = '%s: %s' % (self._get_metadata_by_key('title'), + self._get_metadata_by_key('description')) + + self.connect('activate', self._twitter_share_menu_cb) + + def _get_metadata_by_key(self, key, default_value=''): + if key in self._metadata: + return self._metadata[key] + return default_value + + def _status_updated_cb(self, status, data, tmp_file): + if os.path.exists(tmp_file): + os.unlink(tmp_file) + try: + ds_object = datastore.get(self._metadata['uid']) + ds_object.metadata['twr_object_id'] = status._status_id + datastore.write(ds_object, update_mtime=False) + except Exception as e: + logging.debug('_status_updated_cb failed to write: %s', str(e)) + + def _status_updated_failed_cb(self, status, message, tmp_file): + if os.path.exists(tmp_file): + os.unlink(tmp_file) + logging.error('_status_updated_failed_cb %s', message) + + def _twitter_share_menu_cb(self, menu_item): + logging.debug('_twitter_share_menu_cb') + + self.emit('transfer-state-changed', _('Download started')) + tmp_file = tempfile.mktemp() + self._image_file_from_metadata(tmp_file) + + status = TwrStatus() + status.connect('status-updated', self._status_updated_cb, tmp_file) + status.connect('status-updated-failed', + self._status_updated_failed_cb, tmp_file) + status.update_with_media(self._comment, tmp_file) + + def _image_file_from_metadata(self, image_path): + """ Load a pixbuf from a Journal object. """ + pixbufloader = \ + GdkPixbuf.PixbufLoader.new_with_mime_type('image/png') + pixbufloader.set_size(300, 225) + try: + pixbufloader.write(self._metadata['preview']) + pixbuf = pixbufloader.get_pixbuf() + except Exception as ex: + logging.debug("_image_file_from_metadata: %s" % (str(ex))) + pixbuf = None + + pixbufloader.close() + if pixbuf: + pixbuf.savev(image_path, 'png', [], []) + + +class _TwitterRefreshMenu(account.MenuItem): + def __init__(self, is_active): + account.MenuItem.__init__(self, ACCOUNT_NAME) + + self._is_active = is_active + self._metadata = None + + if is_active: + icon_name = 'twitter-refresh' + else: + icon_name = 'twitter-refresh-insensitive' + self.set_image(Icon(icon_name=icon_name, + icon_size=Gtk.IconSize.MENU)) + self.show() + + self.connect('activate', self._twr_refresh_menu_clicked_cb) + + def set_metadata(self, metadata): + self._metadata = metadata + if self._is_active: + if self._metadata: + if 'fb_object_id' in self._metadata: + self.set_sensitive(True) + icon_name = 'twitter-refresh' + else: + self.set_sensitive(False) + icon_name = 'twitter-refresh-insensitive' + self.set_image(Icon(icon_name=icon_name, + icon_size=Gtk.IconSize.MENU)) + + def _twr_refresh_menu_clicked_cb(self, button): + logging.debug('_twr_refresh_menu_clicked_cb') + + if self._metadata is None: + logging.debug('_twr_refresh_menu_clicked_cb called without metadata') + return + + if 'twr_object_id' not in self._metadata: + logging.debug('_twr_refresh_menu_clicked_cb called without twr_object_id in metadata') + return + + self.emit('transfer-state-changed', _('Download started')) + + status_id = self._metadata['twr_object_id'] + + # XXX is there other way to update metadata? + ds_object = datastore.get(self._metadata['uid']) + if COMMENT_LAST_ID in ds_object.metadata: + status_id = ds_object.metadata[COMMENT_LAST_ID] + + timeline = TwrTimeline() + timeline.connect('mentions-downloaded', self._twr_mentions_downloaded_cb) + timeline.mentions_timeline(since_id=status_id) + + def _twr_mentions_downloaded_cb(self, timeline, comments): + logging.debug('_twr_mentions_downloaded_cb') + + ds_object = datastore.get(self._metadata['uid']) + if not COMMENTS in ds_object.metadata: + ds_comments = [] + else: + ds_comments = json.loads(ds_object.metadata[COMMENTS]) + if not COMMENT_IDS in ds_object.metadata: + ds_comment_ids = [] + else: + ds_comment_ids = json.loads(ds_object.metadata[COMMENT_IDS]) + + comment_id = None + new_comment = False + for comment in comments: + # XXX hope for a better API + if comment['in_reply_to_status_id_str'] != self._metadata['twr_object_id']: + continue + + if comment['id_str'] not in ds_comment_ids: + ds_comments.append({'from': comment['user']['name'], + 'message': comment['text'], + 'icon': 'twitter-share'}) + ds_comment_ids.append(comment['id_str']) + + # XXX keep the latest one + if comment['id_str'] > comment_id: + comment_id = comment['id_str'] + new_comment = True + + if new_comment: + ds_object.metadata[COMMENT_LAST_ID] = comment_id + ds_object.metadata[COMMENTS] = json.dumps(ds_comments) + ds_object.metadata[COMMENT_IDS] = json.dumps(ds_comment_ids) + self.emit('comments-changed', ds_object.metadata[COMMENTS]) + + datastore.write(ds_object, update_mtime=False) + + def _twr_comments_download_failed_cb(self, tweet, failed_reason): + logging.debug('_twr_comments_download_failed_cb: %s' % (failed_reason)) + +def get_account(): + return TwitterAccount() diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py index d5a3da4..bdd16c3 100644 --- a/src/jarabe/journal/journaltoolbox.py +++ b/src/jarabe/journal/journaltoolbox.py @@ -47,7 +47,7 @@ from jarabe.journal import model from jarabe.journal.palettes import ClipboardMenu from jarabe.journal.palettes import VolumeMenu from jarabe.journal import journalwindow -from jarabe.web import online_accounts_manager as oam +from jarabe.web import accountsmanager _AUTOSEARCH_TIMEOUT = 1000 @@ -383,11 +383,12 @@ class DetailToolbox(ToolbarBox): self._duplicate.connect('clicked', self._duplicate_clicked_cb) self.toolbar.insert(self._duplicate, -1) - if len(oam.OnlineAccountsManager.configured_accounts()) > 0: + if len(accountsmanager.get_configured_accounts()) > 0: self._refresh = ToolButton() icon = Icon(icon_name='refresh', xo_color=color) self._refresh.set_icon_widget(icon) icon.show() + self._refresh.set_tooltip(_('Refresh')) self._refresh.connect('clicked', self._refresh_clicked_cb) self.toolbar.insert(self._refresh, -1) @@ -416,6 +417,9 @@ class DetailToolbox(ToolbarBox): def _copy_clicked_cb(self, button): button.palette.popup(immediate=True, state=Palette.SECONDARY) + def _refresh_clicked_cb(self, button): + button.palette.popup(immediate=True, state=Palette.SECONDARY) + def _duplicate_clicked_cb(self, button): file_path = model.get_file(self._metadata['uid']) try: @@ -426,9 +430,6 @@ class DetailToolbox(ToolbarBox): _('Error while copying the entry. %s') % (e.strerror, ), _('Error')) - def _refresh_clicked_cb(self, button): - button.palette.popup(immediate=True, state=Palette.SECONDARY) - def _erase_button_clicked_cb(self, button): alert = Alert() erase_string = _('Erase') @@ -509,7 +510,7 @@ class DetailToolbox(ToolbarBox): palette.menu.append(volume_menu) volume_menu.show() - for account in oam.OnlineAccountsManager.configured_accounts(): + for account in accountsmanager.get_configured_accounts(): menu = account.get_share_menu(self._metadata) palette.menu.append(menu) @@ -525,13 +526,13 @@ class DetailToolbox(ToolbarBox): self._duplicate.hide() def _refresh_refresh_palette(self): + color = misc.get_icon_color(self._metadata) + self._refresh.get_icon_widget().props.xo_color = color palette = self._refresh.get_palette() - for menu_item in palette.menu.get_children(): palette.menu.remove(menu_item) - menu_item.destroy() - for account in oam.OnlineAccountsManager.configured_accounts(): + for account in accountsmanager.get_configured_accounts(): menu = account.get_refresh_menu() palette.menu.append(menu) menu.set_metadata(self._metadata) diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py index ee484a7..5ba959e 100644 --- a/src/jarabe/journal/palettes.py +++ b/src/jarabe/journal/palettes.py @@ -39,7 +39,7 @@ from jarabe.model import mimeregistry from jarabe.journal import misc from jarabe.journal import model from jarabe.journal import journalwindow -from jarabe.web import online_accounts_manager as oam +from jarabe.web import accountsmanager as oam class ObjectPalette(Palette): @@ -252,7 +252,7 @@ class CopyMenu(Gtk.Menu): self.append(volume_menu) volume_menu.show() - for account in oam.OnlineAccountsManager.configured_accounts(): + for account in oam.get_configured_accounts(): menu = account.get_share_menu(metadata) self.append(menu) diff --git a/src/jarabe/web/Makefile.am b/src/jarabe/web/Makefile.am index b751410..a2caada 100644 --- a/src/jarabe/web/Makefile.am +++ b/src/jarabe/web/Makefile.am @@ -1,5 +1,5 @@ sugardir = $(pythondir)/jarabe/web sugar_PYTHON = \ - __init__.py \ - online_account.py \ - online_accounts_manager.py + __init__.py \ + account.py \ + accountsmanager.py diff --git a/src/jarabe/web/__init__.py b/src/jarabe/web/__init__.py index 1e7e0f9..e69de29 100644 --- a/src/jarabe/web/__init__.py +++ b/src/jarabe/web/__init__.py @@ -1,18 +0,0 @@ -"""Graphics/controls for use in Sugar""" - -# Copyright (C) 2006-2007, Red Hat, Inc. -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the -# Free Software Foundation, Inc., 59 Temple Place - Suite 330, -# Boston, MA 02111-1307, USA. diff --git a/src/jarabe/web/account.py b/src/jarabe/web/account.py new file mode 100644 index 0000000..f9be23e --- /dev/null +++ b/src/jarabe/web/account.py @@ -0,0 +1,78 @@ +# Copyright (c) 2013 Walter Bender, Raul Gutierrez Segales +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging + +from gi.repository import GObject + +from sugar3.graphics.menuitem import MenuItem +from sugar3.graphics.toolbutton import ToolButton + + +class Account(): + ''' Account is a prototype class for online accounts. It provides + stubs for five public methods that are used by online services: + + get_description returns a brief description of the online service + used in palette menuitems and on the webservices control panel. + + is_configured returns True if the service has been configured for + use, e.g., an access token has been acquired. + + is_active returns True if the service is currently available, + e.g., the access token has not expired. + + get_share_menu returns a menu item used on the Copy To palette in + the Journal and on the Journal detail view toolbar. + + get_refresh_menu returns a menu item used on the Journal detail + view toolbar. + ''' + + def get_description(self): + raise NotImplementedError + + def is_configured(self): + raise NotImplementedError + + def is_active(self): + raise NotImplementedError + + def get_share_menu(self): + raise NotImplementedError + + def get_refresh_menu(self): + raise NotImplementedError + + +class MenuItem(MenuItem): + __gsignals__ = { + 'transfer-started': (GObject.SignalFlags.RUN_FIRST, None, + ([int, int])), + 'transfer-progress': (GObject.SignalFlags.RUN_FIRST, None, + ([int, int, float])), + 'transfer-completed': (GObject.SignalFlags.RUN_FIRST, None, + ([int, int])), + 'transfer-failed': (GObject.SignalFlags.RUN_FIRST, None, + ([int, int, str])), + 'transfer-state-changed': (GObject.SignalFlags.RUN_FIRST, None, + ([str])), + 'comments-changed': (GObject.SignalFlags.RUN_FIRST, None, ([str])), + } + + def set_metadata(self, metadata): + raise NotImplementedError diff --git a/src/jarabe/web/accountsmanager.py b/src/jarabe/web/accountsmanager.py new file mode 100644 index 0000000..be083c0 --- /dev/null +++ b/src/jarabe/web/accountsmanager.py @@ -0,0 +1,85 @@ +# Copyright (c) 2013 Walter Bender +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import os +import logging + +from gi.repository import Gtk + +from jarabe import config + + +def get_all_accounts(): + accounts = [] + + web_path = os.path.join(config.ext_path, 'web') + if os.path.exists(web_path): + for d in os.listdir(web_path): + dir_path = os.path.join(web_path, d) + module = _load_module(dir_path) + if module is not None: + accounts.append(module) + _load_icon_path(dir_path) + + return accounts + + +def _load_module(dir_path): + module = None + if os.path.isdir(dir_path): + for f in os.listdir(dir_path): + if f.endswith('.py') and not f.startswith('__'): + module_name = f[:-3] + logging.debug('OnlineAccountsManager loading %s' % ( + module_name)) + + try: + mod = __import__( + 'web.' + os.path.basename(dir_path) + '.' + \ + module_name, + globals(), + locals(), + [module_name]) + if hasattr(mod, 'get_account'): + module = mod.get_account() + + except Exception as e: + logging.exception('Exception while loading %s: %s' % ( + module_name, str(e))) + + return module + + +def _load_icon_path(dir_path): + icon_theme = Gtk.IconTheme.get_default() + icon_search_path = icon_theme.get_search_path() + + if os.path.isdir(dir_path): + for f in os.listdir(dir_path): + if f == 'icons': + icon_path = os.path.join(dir_path, f) + if os.path.isdir(icon_path) and \ + icon_path not in icon_search_path: + icon_theme.append_search_path(icon_path) + + +def get_configured_accounts(): + return [a for a in get_all_accounts() if a.is_configured()] + + +def get_active_accounts(): + return [a for a in get_all_accounts() if a.is_active()] |