[Sugar-devel] [PATCH] sl#3315: Journal-Entry transfer from 1-to-N users.

Ajay Garg ajay at activitycentral.com
Fri Feb 3 01:41:13 EST 2012


---

The workflow can be assessed via the screenshots at ::
http://wiki.sugarlabs.org/go/Features/Transfer_to_many_screenshots


 configure.ac                                   |    1 +
 data/icons/Makefile.am                         |    1 +
 data/icons/module-configuration.svg            |  190 +++++++++++
 extensions/cpsection/Makefile.am               |    1 +
 extensions/cpsection/configuration/Makefile.am |    6 +
 extensions/cpsection/configuration/__init__.py |   22 ++
 extensions/cpsection/configuration/model.py    |   21 ++
 extensions/cpsection/configuration/view.py     |  432 ++++++++++++++++++++++++
 src/jarabe/frame/activitiestray.py             |   69 ++++-
 src/jarabe/journal/palettes.py                 |  128 +++++++-
 src/jarabe/model/buddy.py                      |   12 +
 src/jarabe/model/filetransfer.py               |   18 +-
 src/jarabe/model/friends.py                    |  267 ++++++++++++++-
 src/jarabe/view/buddymenu.py                   |   87 +++++-
 14 files changed, 1222 insertions(+), 33 deletions(-)
 create mode 100644 data/icons/module-configuration.svg
 create mode 100644 extensions/cpsection/configuration/Makefile.am
 create mode 100644 extensions/cpsection/configuration/__init__.py
 create mode 100644 extensions/cpsection/configuration/model.py
 create mode 100644 extensions/cpsection/configuration/view.py

diff --git a/configure.ac b/configure.ac
index fa7165c..a70621d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -52,6 +52,7 @@ data/sugar-emulator.desktop
 extensions/cpsection/aboutcomputer/Makefile
 extensions/cpsection/accessibility/Makefile
 extensions/cpsection/aboutme/Makefile
+extensions/cpsection/configuration/Makefile
 extensions/cpsection/datetime/Makefile
 extensions/cpsection/frame/Makefile
 extensions/cpsection/keyboard/Makefile
diff --git a/data/icons/Makefile.am b/data/icons/Makefile.am
index 8e01626..60f735a 100644
--- a/data/icons/Makefile.am
+++ b/data/icons/Makefile.am
@@ -4,6 +4,7 @@ sugar_DATA =                        \
 	module-about_me.svg             \
 	module-about_my_computer.svg    \
 	module-accessibility.svg        \
+	module-configuration.svg        \
 	module-date_and_time.svg        \
 	module-frame.svg                \
 	module-keyboard.svg             \
diff --git a/data/icons/module-configuration.svg b/data/icons/module-configuration.svg
new file mode 100644
index 0000000..16ca355
--- /dev/null
+++ b/data/icons/module-configuration.svg
@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.1 r9760"
+   sodipodi:docname="module-configuration.svg">
+  <defs
+     id="defs4">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective6637" />
+    <inkscape:perspective
+       id="perspective6615"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.35"
+     inkscape:cx="375"
+     inkscape:cy="514.28571"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1366"
+     inkscape:window-height="693"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="layer1-3"
+       inkscape:label="Layer 1"
+       transform="matrix(2.0112611,0,0,2.8271726,-382.79436,-991.72999)">
+      <g
+         inkscape:label="Layer 1"
+         id="layer1-1"
+         transform="translate(170.0671,-314.28571)">
+        <g
+           transform="matrix(9.8137136,0,0,9.8137136,-2250.1262,598.08659)"
+           id="g6596">
+          <g
+             id="g7197">
+            <path
+               sodipodi:type="arc"
+               style="fill:none;stroke:#00000f;stroke-width:9.03419971;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+               id="path5989"
+               sodipodi:cx="307.3714"
+               sodipodi:cy="24.611456"
+               sodipodi:rx="12.380237"
+               sodipodi:ry="11.953332"
+               d="m 319.75164,24.611456 c 0,6.601643 -5.54283,11.953332 -12.38024,11.953332 -6.83742,0 -12.38024,-5.351689 -12.38024,-11.953332 0,-6.601643 5.54282,-11.953332 12.38024,-11.953332 6.83741,0 12.38024,5.351689 12.38024,11.953332 z"
+               transform="matrix(0.77926228,0,0,0.79380856,10.613376,6.1120011)" />
+            <rect
+               style="fill:#00000f;fill-opacity:1;stroke:none"
+               id="rect6505"
+               width="7.9117804"
+               height="9.2506971"
+               x="246.11057"
+               y="7.1083627"
+               ry="0.82999998"
+               rx="0.82999998" />
+            <rect
+               style="fill:#00000f;fill-opacity:1;stroke:none"
+               id="rect6505-9"
+               width="7.9117804"
+               height="9.2506971"
+               x="246.21878"
+               y="35.461403"
+               rx="0.82999998"
+               ry="0.82999998" />
+            <rect
+               style="fill:#00000f;fill-opacity:1;stroke:none"
+               id="rect6505-7"
+               width="7.9117804"
+               height="9.2506971"
+               x="-29.96331"
+               y="231.45294"
+               transform="matrix(0,-1,1,0,0,0)"
+               ry="0.82999998"
+               rx="0.82999998" />
+            <rect
+               style="fill:#00000f;fill-opacity:1;stroke:none"
+               id="rect6505-6"
+               width="7.9117804"
+               height="9.2506971"
+               x="-29.848055"
+               y="259.62869"
+               transform="matrix(0,-1,1,0,0,0)"
+               ry="0.82999998"
+               rx="0.82999998" />
+            <rect
+               style="fill:#00000f;fill-opacity:1;stroke:none"
+               id="rect6505-76"
+               width="7.9117804"
+               height="9.2506971"
+               x="154.41437"
+               y="176.47606"
+               transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
+               ry="0.82999998"
+               rx="0.82999998" />
+            <rect
+               style="fill:#00000f;fill-opacity:1;stroke:none"
+               id="rect6505-76-7"
+               width="7.9117804"
+               height="9.2506971"
+               x="-199.0881"
+               y="-177.31622"
+               transform="matrix(-0.70710678,-0.70710678,-0.70710678,0.70710678,0,0)"
+               ry="0.82999998"
+               rx="0.82999998" />
+            <rect
+               style="fill:#00000f;fill-opacity:1;stroke:none"
+               id="rect6505-76-5"
+               width="7.9117804"
+               height="9.2506971"
+               x="154.6358"
+               y="204.75911"
+               transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
+               ry="0.82999998"
+               rx="0.82999998" />
+            <rect
+               style="fill:#00000f;fill-opacity:1;stroke:none"
+               id="rect6505-76-0"
+               width="7.9117804"
+               height="9.2506971"
+               x="-199.24612"
+               y="-148.95982"
+               transform="matrix(-0.70710678,-0.70710678,-0.70710678,0.70710678,0,0)"
+               ry="0.82999998"
+               rx="0.82999998" />
+          </g>
+        </g>
+      </g>
+      <path
+         inkscape:connector-curvature="0"
+         id="path3073"
+         d="M 340.90476,719.02884 C 339.85714,717.98124 339,703.77275 339,687.4545 l 0,-29.6696 -10.32864,-4.3156 -10.32863,-4.31557 -22.38347,21.78237 c -18.44567,17.95034 -23.2886,21.29505 -27.52851,19.01228 -6.09928,-3.28383 -46.57361,-44.63814 -46.57361,-47.5862 0,-1.12068 9.16077,-11.48513 20.35725,-23.03211 l 20.35725,-20.99453 -4.31522,-10.84382 -4.31523,-10.84382 -29.61345,-1.42857 -29.61345,-1.42858 -0.80918,-34.05845 c -0.60171,-25.32665 0.13079,-34.65488 2.85715,-36.38482 2.01647,-1.2795 15.35566,-2.34605 29.64263,-2.37012 l 25.97632,-0.0437 3.54536,-11.83336 3.54537,-11.83336 -18.8074,-18.95263 c -10.34407,-10.42395 -18.8074,-20.23446 -18.8074,-21.80115 0,-1.56668 10.92551,-13.71726 24.2789,-27.00129 l 24.2789,-24.15277 19.97202,19.81898 19.97202,19.81898 12.17765,-4.4318 12.17766,-4.4318 0.71651,-24.02847 c 1.19845,-40.19036 -1.99777,-37.19155 38.64555,-36.25866 L 409,356.6479 l 0.80987,28.88986 0.80986,28.88986 11.95928,4.69773 11.95928,4.69774 18.73113,-18.5876 c 10.30212,-10.22317 20.65581,-18.58759 23.00821,-18.58759 2.3524,0 15.0678,10.84703 28.2564,24.10452 l 23.97931,24.10453 -20.52888,21.17152 -20.52889,21.17153 4.32606,8.29538 c 2.37931,4.56245 4.33394,9.85754 4.34363,11.76685 0.0114,2.44368 8.68823,3.71255 29.30331,4.28572 l 29.28572,0.81423 0,35.71429 0,35.71428 -29.28915,0.81428 -29.28914,0.81428 -4.3258,10.35309 -4.3258,10.3531 20.04351,20.67097 c 11.02395,11.36904 20.04352,22.24137 20.04352,24.16074 0,5.18526 -43.1282,48.54783 -48.28543,48.54783 -2.44054,0 -13.64577,-9.14697 -24.90053,-20.32663 l -20.46319,-20.3266 -10.31828,3.40534 -10.31829,3.40532 0,28.11617 c 0,15.46391 -0.78041,30.14991 -1.73425,32.63557 -1.44498,3.76557 -7.32266,4.5194 -35.23809,4.5194 -18.42712,0 -34.36099,-0.85714 -35.40861,-1.90477 l 0,0 z M 394.3828,591.70551 c 16.14693,-4.82459 33.99418,-23.23962 38.65434,-39.88406 7.17003,-25.60883 -3.3713,-52.07528 -26.41559,-66.32247 -11.46728,-7.08967 -16.74237,-8.49394 -31.90726,-8.49394 -25.11984,0 -42.50709,10.48723 -53.57143,32.31195 -12.59921,24.85226 -8.97102,47.7484 10.68602,67.43544 17.12848,17.15462 38.27107,22.20862 62.55392,14.95308 l 0,0 z"
+         style="fill:#a0a0a0;stroke:none" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path3075"
+         d="m 340.31812,686.89964 -2.03213,-31.35394 -10.35728,-4.13652 -10.35728,-4.13651 -22.11154,21.11617 c -12.16135,11.61391 -23.18839,21.1162 -24.50454,21.1162 -1.31614,0 -12.4099,-10.29334 -24.65281,-22.87409 -25.3507,-26.05026 -25.50888,-21.71631 1.82765,-50.07477 l 16.61352,-17.23457 -4.608,-11.73082 -4.608,-11.73082 -29.78067,-2.02985 -29.78067,-2.02985 0.80253,-33.29047 0.80253,-33.29047 28.48749,-2.85715 28.48749,-2.85714 3.28334,-11.42857 3.28334,-11.42857 -18.1994,-19.00791 c -10.00967,-10.45436 -18.1994,-20.12738 -18.1994,-21.4956 0,-1.36824 10.28354,-12.71092 22.85231,-25.20598 l 22.85231,-22.71828 19.3964,19.24778 19.3964,19.24777 13.13154,-4.47437 13.13155,-4.47437 1.15055,-26.27381 c 0.63281,-14.45059 2.14472,-27.88095 3.3598,-29.84523 1.55902,-2.52031 11.82996,-3.57143 34.89776,-3.57143 l 32.68852,0 0,22.67114 c 0,30.00654 2.58714,36.16256 17.01434,40.48506 l 11.55808,3.46288 18.32364,-17.59525 c 10.078,-9.67739 19.89161,-17.59526 21.80802,-17.59526 1.9164,0 13.92055,10.23947 26.67586,22.75438 l 23.19149,22.75438 -19.88883,20.06797 -19.88886,20.06796 4.43392,12.17766 c 4.09622,11.25017 5.3934,12.32472 17.03171,14.10873 6.92877,1.06209 19.99063,2.02637 29.02634,2.14285 l 16.42857,0.21178 0,34.28572 0,34.28571 -21.07142,0 c -33.44695,0 -38.35323,1.70239 -42.23369,14.65427 l -3.32149,11.0861 19.02758,19.70916 c 10.4652,10.84003 19.0276,21.34799 19.0276,23.35102 0,4.76402 -40.74506,45.48517 -45.51189,45.48517 -2.01771,0 -12.90991,-8.96083 -24.20489,-19.91295 -20.44586,-19.82525 -20.58136,-19.89914 -30.76296,-16.77431 l -10.22664,3.13866 -2.50396,31.77428 L 409,716.6479 l -33.32487,0.80283 -33.32488,0.80282 -2.03213,-31.35391 z m 58.05936,-96.11759 c 41.48929,-17.33107 50.46415,-70.92953 16.82283,-100.467 -5.72605,-5.02754 -15.82082,-10.5977 -22.43283,-12.37813 -14.95988,-4.02828 -38.40229,-1.33506 -50.05351,5.75047 -22.67204,13.78769 -35.67939,49.00392 -26.7625,72.45711 3.99028,10.49523 19.70634,27.24005 31.3352,33.38637 12.72992,6.72828 36.58078,7.31236 51.09081,1.25118 z"
+         style="fill:#a0a0a0;stroke:none" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path3077"
+         d="m 340.31239,686.8111 -2.0264,-31.2654 -10.35728,-4.13652 -10.35728,-4.13651 -22.11154,21.11617 c -12.16135,11.61391 -23.18839,21.1162 -24.50454,21.1162 -1.31614,0 -12.4099,-10.29334 -24.65281,-22.87409 -25.3507,-26.05026 -25.50888,-21.71631 1.82765,-50.07477 l 16.61352,-17.23457 -4.73383,-12.05114 c -4.33898,-11.04598 -5.59133,-12.05284 -15.01471,-12.07146 -5.65448,-0.0112 -18.95946,-0.83994 -29.5666,-1.84172 l -19.28571,-1.82142 0,-33.48294 0,-33.48293 29.21025,-2.53248 29.21025,-2.53248 3.27487,-11.42857 3.27486,-11.42857 -18.1994,-19.00791 c -10.00967,-10.45436 -18.1994,-20.12738 -18.1994,-21.4956 0,-1.36824 10.28354,-12.71092 22.85231,-25.20598 l 22.85231,-22.71828 19.3964,19.24778 19.3964,19.24777 13.13154,-4.47437 13.13155,-4.47437 1.15055,-26.27381 c 0.63281,-14.45059 2.14472,-27.88095 3.3598,-29.84523 1.55902,-2.52031 11.82996,-3.57143 34.89776,-3.57143 l 32.68852,0 0.2848,22.14286 c 0.406,31.56613 2.03422,35.66433 16.12267,40.58042 l 11.8983,4.15186 18.45696,-17.72329 c 10.15133,-9.7478 20.02494,-17.72328 21.94135,-17.72328 1.9164,0 13.92055,10.23947 26.67586,22.75438 l 23.19149,22.75438 -19.88883,20.06797 -19.88886,20.06796 4.43392,12.17766 c 4.09622,11.25017 5.3934,12.32472 17.03171,14.10873 6.92877,1.06209 19.99063,2.02637 29.02634,2.14285 l 16.42857,0.21178 0,34.28572 0,34.28571 -23.57142,0.0421 c -27.72475,0.0495 -36.66832,3.7013 -40.32949,16.46697 -2.54951,8.88964 -1.8394,10.14932 16.33206,28.97166 10.44857,10.82287 18.99743,21.31677 18.99743,23.3198 0,4.74805 -40.73712,45.48517 -45.48518,45.48517 -2.00302,0 -12.29835,-8.35715 -22.87852,-18.57143 -18.70259,-18.05583 -23.52162,-20.28709 -36.37094,-16.84 -5.69473,1.52771 -6.31799,4.13083 -8.1997,34.24668 l -2.03651,32.59332 -33.70233,0 -33.70233,0 -2.02639,-31.26537 z m 58.06509,-96.02905 c 31.3829,-13.10939 46.15865,-49.6639 32.35734,-80.05056 -16.96561,-37.35354 -70.38585,-46.62407 -98.95583,-17.17275 -24.62497,25.38461 -24.40007,61.43104 0.53077,85.07441 16.93513,16.06056 44.54805,21.13818 66.06772,12.1489 l 0,0 z"
+         style="fill:#ffffff;stroke:none" />
+    </g>
+  </g>
+</svg>
diff --git a/extensions/cpsection/Makefile.am b/extensions/cpsection/Makefile.am
index 2074d11..fef9e52 100644
--- a/extensions/cpsection/Makefile.am
+++ b/extensions/cpsection/Makefile.am
@@ -2,6 +2,7 @@ SUBDIRS = \
 	aboutme \
 	aboutcomputer \
 	accessibility \
+	configuration \
 	datetime \
 	frame \
 	keyboard \
diff --git a/extensions/cpsection/configuration/Makefile.am b/extensions/cpsection/configuration/Makefile.am
new file mode 100644
index 0000000..9f3718a
--- /dev/null
+++ b/extensions/cpsection/configuration/Makefile.am
@@ -0,0 +1,6 @@
+sugardir = $(pkgdatadir)/extensions/cpsection/configuration
+
+sugar_PYTHON = 		\
+	__init__.py	\
+	model.py	\
+	view.py		
diff --git a/extensions/cpsection/configuration/__init__.py b/extensions/cpsection/configuration/__init__.py
new file mode 100644
index 0000000..dd61992
--- /dev/null
+++ b/extensions/cpsection/configuration/__init__.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2012 Simon Schampijer <erikos at sugarlabs.org>
+# Copyright (C) 2012 Ajay Garg        <ajay at activitycentral.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+from gettext import gettext as _
+
+CLASS = 'Configuration'
+ICON = 'module-configuration'
+TITLE = _('Configuration')
diff --git a/extensions/cpsection/configuration/model.py b/extensions/cpsection/configuration/model.py
new file mode 100644
index 0000000..4fd798d
--- /dev/null
+++ b/extensions/cpsection/configuration/model.py
@@ -0,0 +1,21 @@
+# Copyright (C) 2012 Simon Schampijer <erikos at sugarlabs.org>
+# Copyright (C) 2012 Ajay Garg        <ajay at activitycentral.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import logging
+
+_logger = logging.getLogger('ControlPanel - Configuration')
diff --git a/extensions/cpsection/configuration/view.py b/extensions/cpsection/configuration/view.py
new file mode 100644
index 0000000..8389fd3
--- /dev/null
+++ b/extensions/cpsection/configuration/view.py
@@ -0,0 +1,432 @@
+# Copyright (C) 2012 Simon Schampijer <erikos at sugarlabs.org>
+# Copyright (C) 2012 Ajay Garg        <ajay at activitycentral.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+
+import gtk
+import gobject
+from gettext import gettext as _
+
+from sugar.graphics import style
+from sugar.graphics.alert import Alert
+from sugar.graphics.icon import Icon
+
+from jarabe.model import buddy
+from jarabe.model import friends
+from jarabe.model import neighborhood
+
+from jarabe.controlpanel.sectionview import SectionView
+from jarabe.controlpanel.inlinealert import InlineAlert
+
+owner_model = buddy.get_owner_instance()
+friends_model = friends.get_model()
+neighborhood_model = neighborhood.get_model()
+
+class LeftAlignedLabelWidget(gtk.HBox):
+
+    def __init__(self, label):
+        gtk.HBox.__init__(self)
+        label_widget = gtk.Label(label)
+        label_widget.set_line_wrap(True)
+        self.pack_start(label_widget, expand=False)
+        self.show_all()
+
+
+class CenterAlignedLabelWidget(gtk.VBox):
+
+    def __init__(self, label):
+        gtk.VBox.__init__(self)
+        label_widget = gtk.Label(label)
+        label_widget.set_line_wrap(True)
+        self.pack_start(label_widget, expand=False)
+        self.show_all()
+
+
+class BuddyWidget(gtk.HBox):
+
+    def __init__(self, buddy):
+        gtk.HBox.__init__(self)
+        self._buddy = buddy
+
+        self._check_button = gtk.CheckButton(label=self._buddy.nick)
+        self._check_button.set_active(False)
+        self.pack_start(self._check_button, expand=False)
+        self.show_all()
+
+    def _is_buddy_selected(self):
+        return self._check_button.get_active()
+
+    def _get_buddy_key(self):
+        return self._buddy.key
+
+    def _get_buddy_nick(self):
+        return self._buddy.nick
+
+    def _get_buddy_account(self):
+        if hasattr(self._buddy, 'account'):
+            return self._buddy.account
+        return None
+
+    def _get_buddy_contact_id(self):
+        if hasattr(self._buddy, 'contact_id'):
+            return self._buddy.contact_id
+        return None
+
+
+class AddRemoveWidget(gtk.VBox):
+
+    def __init__(self, label, group_detail, add_button_clicked_cb,
+                 remove_button_clicked_cb, index):
+        gtk.VBox.__init__(self)
+        self.set_homogeneous(False)
+        self.set_spacing(10)
+
+        self._potential_new_group = False
+        self._group_name = label
+        self._group_detail = group_detail
+
+        self._primary_box = gtk.HBox()
+        self._primary_box.set_homogeneous(False)
+        self._primary_box.set_spacing(10)
+        self.pack_start(self._primary_box, expand=False)
+        self._primary_box.show_all()
+
+        self._index = index
+        self._add_button_added = False
+        self._remove_button_added = False
+
+        self._label = gtk.Entry()
+        self._label.set_text(label)
+
+        # Do not allow an already existing group name to be modified.
+        if len(label) > 0:
+            self._label.set_sensitive(False)
+        # Else, this is a potentially new group.
+        # Mark this is as new.
+        # However, this will ACTUALLY be a new group,
+        # only if it is given a name.
+        # That check will be done, after the user clicks the final save
+        # button.
+        else:
+            self._potential_new_group = True
+
+        self._primary_box.pack_start(self._label, expand=False)
+
+        if not self._potential_new_group:
+            self._details_button = gtk.Button(_('View Details'))
+            self._view_id = self._details_button.connect('clicked', self.__view_details_cb)
+            self._primary_box.pack_start(self._details_button,
+                    expand=False)
+
+        add_icon = Icon(icon_name='list-add')
+        self._add_button = gtk.Button()
+        self._add_button.set_image(add_icon)
+        self._add_button.connect('clicked',
+                                 add_button_clicked_cb,
+                                 self)
+
+        remove_icon = Icon(icon_name='list-remove')
+        self._remove_button = gtk.Button()
+        self._remove_button.set_image(remove_icon)
+        self._remove_button.connect('clicked',
+                                    remove_button_clicked_cb,
+                                    self)
+
+        self.__add_add_button()
+        self.__add_remove_button()
+
+        self._details_table = gtk.VBox()
+        self._details_table.set_spacing(20)
+        self._details_table.show_all()
+        self.pack_start(self._details_table, expand=False)
+
+        if self._potential_new_group:
+            info_label = LeftAlignedLabelWidget(_('You may batch add'
+                ' from the following available online buddies. Any'
+                ' currently offline buddy may be added later from the'
+                ' neighborhood view, when it comes online.'))
+            self.pack_start(info_label, expand=False)
+            self._friends_list_box = gtk.VBox()
+            self._friends_list_box.set_homogeneous(True)
+            self._friends_list_box.set_spacing(20)
+            self.pack_start(self._friends_list_box, expand=False)
+            self._populate_friends_list()
+
+        self.pack_start(gtk.HSeparator())
+
+        self._primary_box.show_all()
+        self.show_all()
+
+    def _populate_friends_list(self):
+        buddies = neighborhood_model.get_buddies()
+        for buddy in buddies:
+
+            # Only make the buddy visible, if it has a valid key at
+            # this time.
+            if ((hasattr(buddy, 'key')) and (buddy.key is not None)):
+
+                # Do not show self :)
+                if buddy.key == owner_model.get_key():
+                    continue
+
+                self._friends_list_box.pack_start(BuddyWidget(buddy))
+
+        self._friends_list_box.show_all()
+
+    def _get_buddy_widgets(self):
+        return self._friends_list_box.get_children()
+
+    def _get_index(self):
+        return self._index
+
+    def _set_index(self, value):
+        self._index = value
+
+    def _get_entry(self):
+        return self._label.get_text()
+
+    def __add_add_button(self):
+        self._primary_box.pack_start(self._add_button, expand=False)
+        self._add_button_added = True
+
+    def _remove_remove_button_if_not_already(self):
+        if self._remove_button_added:
+            self.__remove_remove_button()
+
+    def __remove_remove_button(self):
+        self._primary_box.remove(self._remove_button)
+        self._remove_button_added = False
+
+    def _add_remove_button_if_not_already(self):
+        if not self._remove_button_added:
+            self.__add_remove_button()
+
+    def __add_remove_button(self):
+        self._primary_box.pack_start(self._remove_button, expand=False)
+        self._remove_button_added = True
+
+    def __activate_view_id(self):
+        self._details_button.disconnect(self._hide_id)
+        self._view_id = self._details_button.connect('clicked',
+                self.__view_details_cb)
+
+    def __activate_hide_id(self):
+        self._details_button.disconnect(self._view_id)
+        self._hide_id = self._details_button.connect('clicked',
+                self.__hide_details_cb)
+
+    def __view_details_cb(self, widget):
+        if self._group_detail is None:
+            return
+
+        last_operation_value = friends_model._get_last_group_operation(self._group_name)
+
+        self._last_operation_box = gtk.VBox()
+        self._last_operation_box.pack_start(LeftAlignedLabelWidget(_('Last'
+            ' Operation On This Group :: ')), expand=False)
+        self._last_operation_box.pack_start(CenterAlignedLabelWidget(
+            last_operation_value), expand=False)
+        self._last_operation_box.show_all()
+
+        self._details_table.pack_start(self._last_operation_box,
+                expand=False)
+
+        self._container = gtk.Table()
+        self._details_table.pack_start(self._container, expand=False)
+
+        headings_list = [_('Nick'), _('Last Operation Status')]
+        for i in range(0, len(headings_list)):
+            self._container.attach(gtk.Label(headings_list[i]), i, i+1,
+                    0, 1)
+        for i in range(0, len(headings_list)):
+            self._container.attach(gtk.Label(''), i, i+1, 1, 2)
+
+        index = 2
+        friend_keys_of_group = \
+                friends_model._get_friend_keys_of_group(self._group_name)
+
+        for friend_key in friend_keys_of_group:
+            friend_model = friends_model._get_friend_by_key(friend_key)
+            label_widgets = []
+            nick = friend_model.get_nick()
+            label_widgets.append(CenterAlignedLabelWidget(nick))
+
+            last_operation_status = \
+                    friends_model._get_last_operation_status_of_friend_in_group(self._group_name,
+                                                                                friend_key)
+            label_widgets.append(CenterAlignedLabelWidget(last_operation_status))
+
+            for i in range(0, len(label_widgets)):
+                self._container.attach(label_widgets[i], i, i+1, index,
+                        index+1)
+            index = index + 1
+
+        self._container.show_all()
+
+        self._details_button.set_label(_('Hide Details'))
+        self.__activate_hide_id()
+
+    def __hide_details_cb(self, widget):
+        self._details_table.remove(self._last_operation_box)
+        self._details_table.remove(self._container)
+
+        self._details_button.set_label(_('View Details'))
+        self.__activate_view_id()
+
+
+class MultiWidget(gtk.VBox):
+
+    def __init__(self):
+        gtk.VBox.__init__(self)
+        self.set_spacing(10)
+
+    def _add_widget(self, label, metadata):
+        new_widget = AddRemoveWidget(label,
+                                     metadata,
+                                     self.__add_button_clicked_cb,
+                                     self.__remove_button_clicked_cb,
+                                     len(self.get_children()))
+        self.add(new_widget)
+        self.show_all()
+        self._update_remove_button_statuses()
+
+    def _add_blank_entry(self):
+        self._add_widget('', None)
+
+    def __add_button_clicked_cb(self, add_button,
+                                      add_button_container):
+        self._add_blank_entry()
+        self._update_remove_button_statuses()
+
+    def __remove_button_clicked_cb(self, remove_button,
+                                   remove_button_container):
+        # Remove group from the model.
+        group_name = remove_button_container._get_entry()
+        friends_model.remove_group(group_name)
+
+        # Remove group from the view.
+        self.remove(remove_button_container)
+
+        self._update_remove_button_statuses()
+
+    def _update_remove_button_statuses(self):
+        children = self.get_children()
+
+        # Now, if there is only one entry, remove-button
+        # should not be shown.
+        if len(children) == 1:
+            children[0]._remove_remove_button_if_not_already()
+
+        # Alternatively, if there are more than 1 entries,
+        # remove-button should be shown for all.
+        if len(children) > 1:
+            for child in children:
+                child._add_remove_button_if_not_already()
+
+    def set_groups(self, groups):
+        self._groups = groups
+
+    def _pre_save_operations(self):
+        for child in self.get_children():
+            if child._potential_new_group:
+                group_name = child._get_entry()
+                if len(group_name) > 0:
+                    friends_model.add_group(group_name, False)
+
+                    # Also, add all the selected buddies as friends.
+                    buddy_widgets = child._get_buddy_widgets()
+                    for widget in buddy_widgets:
+                        if widget._is_buddy_selected():
+                            # Add as friend.
+                            friends_model.make_friend_by_parameters(
+                                    widget._get_buddy_key(),
+                                    widget._get_buddy_nick(),
+                                    widget._get_buddy_account(),
+                                    widget._get_buddy_contact_id())
+
+                            # Add as friend in group.
+                            friends_model.add_friend_to_group(widget._get_buddy_key(),
+                                                              group_name,
+                                                              False)
+
+        # Perform just one disk-write for groups.
+        friends_model.save_groups()
+
+
+class Configuration(SectionView):
+    def __init__(self, model, alerts=None):
+        SectionView.__init__(self)
+
+        self._model = model
+
+        self.set_border_width(style.DEFAULT_SPACING * 2)
+        self.set_spacing(style.DEFAULT_SPACING)
+        group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
+
+        workspace = gtk.VBox()
+        workspace.show()
+
+        separator = gtk.HSeparator()
+        workspace.pack_start(separator, expand=False)
+
+        label_friend_groups = gtk.Label(_('Groups configuration'))
+        label_friend_groups.set_alignment(0, 0)
+        workspace.pack_start(label_friend_groups, expand=False)
+
+        box_friend_groups = gtk.VBox()
+        box_friend_groups.set_border_width(style.DEFAULT_SPACING * 2)
+        box_friend_groups.set_spacing(style.DEFAULT_SPACING)
+
+        self._widget_table = MultiWidget()
+        box_friend_groups.pack_start(self._widget_table, expand=False)
+
+        save_button = gtk.Button()
+        save_button.set_alignment(0, 0)
+        save_button.set_label('Save')
+        save_button.connect('clicked', self.__save_button_clicked_cb)
+        box_save_button = gtk.HBox()
+        box_save_button.set_homogeneous(False)
+        box_save_button.pack_start(save_button, expand=False)
+        box_save_button.show_all()
+
+        box_friend_groups.pack_start(box_save_button, expand=False)
+
+        box_friend_groups.show_all()
+        workspace.pack_start(box_friend_groups, expand=False)
+
+        scrolled = gtk.ScrolledWindow()
+        scrolled.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        scrolled.add_with_viewport(workspace)
+        scrolled.show()
+        self.add(scrolled)
+
+        workspace.show_all()
+        self.setup()
+
+    def setup(self):
+        groups = friends_model._get_groups()
+        groups.sort()
+
+        if len(groups) == 0:
+            self._widget_table._add_blank_entry()
+        else:
+            for group_name in groups:
+                group_detail = \
+                        friends_model._get_group_by_key_name(group_name)
+                self._widget_table._add_widget(group_name, group_detail)
+
+    def __save_button_clicked_cb(self, save_button):
+        save_button.set_sensitive(False)
+        self._widget_table._pre_save_operations()
diff --git a/src/jarabe/frame/activitiestray.py b/src/jarabe/frame/activitiestray.py
index 941b174..d2902be 100644
--- a/src/jarabe/frame/activitiestray.py
+++ b/src/jarabe/frame/activitiestray.py
@@ -354,7 +354,9 @@ class BaseTransferButton(ToolButton):
     def remove(self):
         frame = jarabe.frame.get_view()
         frame.remove_notification(self.notif_icon)
-        self.props.parent.remove(self)
+        if (self.props.parent is not None) and \
+                (self in self.props.parent.get_children()):
+            self.props.parent.remove(self)
 
     def __notify_state_cb(self, file_transfer, pspec):
         logging.debug('_update state: %r %r', file_transfer.props.state,
@@ -471,6 +473,13 @@ class OutgoingTransferButton(BaseTransferButton):
         frame.add_notification(self.notif_icon,
                                gtk.CORNER_TOP_LEFT)
 
+        # TODO: figure out why this is necessary to do.
+        #       if this step is not done, then invoking
+        #       "__dismiss_clicked_cb()" WITHOUT clicking the "Dismiss"
+        #       option (as in the case of auto-dismiss for bulk
+        #       operations), DOES NOT WORK.
+        self.create_palette()
+
     def create_palette(self):
         palette = OutgoingTransferPalette(self.file_transfer)
         palette.connect('dismiss-clicked', self.__dismiss_clicked_cb)
@@ -480,6 +489,14 @@ class OutgoingTransferButton(BaseTransferButton):
 
     def __dismiss_clicked_cb(self, palette):
         self.remove()
+        bulk_operation_details = \
+                self.file_transfer._get_bulk_operation_details()
+        if bulk_operation_details is not None:
+            group_name = bulk_operation_details._get_group_name()
+            friend_keys = bulk_operation_details._get_friend_keys()
+            counter = bulk_operation_details._get_counter()
+            proceed_cb = bulk_operation_details._get_proceed_cb()
+            proceed_cb(group_name, friend_keys, counter)
 
 
 class BaseTransferPalette(Palette):
@@ -565,6 +582,7 @@ class IncomingTransferPalette(BaseTransferPalette):
 
     def _update(self):
         logging.debug('_update state: %r', self.file_transfer.props.state)
+
         if self.file_transfer.props.state == filetransfer.FT_STATE_PENDING:
             menu_item = MenuItem(_('Accept'), icon_name='dialog-ok')
             menu_item.connect('activate', self.__accept_activate_cb)
@@ -595,7 +613,6 @@ class IncomingTransferPalette(BaseTransferPalette):
 
         elif self.file_transfer.props.state in \
                 [filetransfer.FT_STATE_ACCEPTED, filetransfer.FT_STATE_OPEN]:
-
             for item in self.menu.get_children():
                 self.menu.remove(item)
 
@@ -619,7 +636,6 @@ class IncomingTransferPalette(BaseTransferPalette):
             self.update_progress()
 
         elif self.file_transfer.props.state == filetransfer.FT_STATE_COMPLETED:
-
             for item in self.menu.get_children():
                 self.menu.remove(item)
 
@@ -629,8 +645,8 @@ class IncomingTransferPalette(BaseTransferPalette):
             menu_item.show()
 
             self.update_progress()
-        elif self.file_transfer.props.state == filetransfer.FT_STATE_CANCELLED:
 
+        elif self.file_transfer.props.state == filetransfer.FT_STATE_CANCELLED:
             for item in self.menu.get_children():
                 self.menu.remove(item)
 
@@ -684,11 +700,21 @@ class OutgoingTransferPalette(BaseTransferPalette):
 
         self.progress_bar = None
         self.progress_label = None
+        self._bulk_operation_details = \
+                file_transfer._get_bulk_operation_details()
 
         self.file_transfer.connect('notify::state', self.__notify_state_cb)
 
         nick = str(file_transfer.buddy.props.nick)
-        label = glib.markup_escape_text(_('Transfer to %s') % (nick,))
+
+        label = None
+        if self._bulk_operation_details is None:
+            label = glib.markup_escape_text(_('Transfer to %s') % (nick,))
+        else:
+            counter = self._bulk_operation_details._get_counter()
+            total = self._bulk_operation_details._get_total()
+            label = glib.markup_escape_text(_('( %d / %d ) Transfer to %s') \
+                    % (counter, total, nick,))
         self.props.secondary_text = label
 
         self._update()
@@ -699,8 +725,8 @@ class OutgoingTransferPalette(BaseTransferPalette):
     def _update(self):
         new_state = self.file_transfer.props.state
         logging.debug('_update state: %r', new_state)
-        if new_state == filetransfer.FT_STATE_PENDING:
 
+        if new_state == filetransfer.FT_STATE_PENDING:
             menu_item = MenuItem(_('Cancel'), icon_name='dialog-cancel')
             menu_item.connect('activate', self.__cancel_activate_cb)
             self.menu.append(menu_item)
@@ -725,7 +751,6 @@ class OutgoingTransferPalette(BaseTransferPalette):
 
         elif new_state in [filetransfer.FT_STATE_ACCEPTED,
                            filetransfer.FT_STATE_OPEN]:
-
             for item in self.menu.get_children():
                 self.menu.remove(item)
 
@@ -750,7 +775,6 @@ class OutgoingTransferPalette(BaseTransferPalette):
 
         elif new_state in [filetransfer.FT_STATE_COMPLETED,
                            filetransfer.FT_STATE_CANCELLED]:
-
             for item in self.menu.get_children():
                 self.menu.remove(item)
 
@@ -758,9 +782,36 @@ class OutgoingTransferPalette(BaseTransferPalette):
             menu_item.connect('activate', self.__dismiss_activate_cb)
             self.menu.append(menu_item)
             menu_item.show()
-
             self.update_progress()
 
+            # Perform actions specific to bulk-operation.
+            if self._bulk_operation_details is not None:
+
+                # Update the peration status.
+                status = None
+                if new_state == filetransfer.FT_STATE_COMPLETED:
+                    status = _('Success.')
+                elif new_state == filetransfer.FT_STATE_CANCELLED:
+                    if self.file_transfer.reason_last_change == \
+                            filetransfer.FT_REASON_REMOTE_STOPPED:
+                        status = _('FAILURE:\tOperation Cancelled Remotely.')
+                    elif self.file_transfer.reason_last_change == \
+                            filetransfer.FT_REASON_LOCAL_STOPPED:
+                        status = _('FAILURE:\tOperation Cancelled Locally.')
+
+                self._set_bulk_operation_status_if_applicable(status)
+
+                # "Dismiss' automatically for all, except the last of
+                # the bulk-operation.
+                counter = self._bulk_operation_details._get_counter()
+                total = self._bulk_operation_details._get_total()
+                if counter < total:
+                    self.__dismiss_activate_cb(None)
+
+    def _set_bulk_operation_status_if_applicable(self, status):
+        if self._bulk_operation_details is not None:
+            self._bulk_operation_details._set_operation_status(status)
+
     def __cancel_activate_cb(self, menu_item):
         self.file_transfer.cancel()
 
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
index 8fc1e5d..e1d1e7d 100644
--- a/src/jarabe/journal/palettes.py
+++ b/src/jarabe/journal/palettes.py
@@ -37,6 +37,39 @@ from jarabe.model import mimeregistry
 from jarabe.journal import misc
 from jarabe.journal import model
 
+friends_model = friends.get_model()
+
+
+class BulkOperationDetails():
+
+    def __init__(self, group_name, friend, friend_keys, total, counter, proceed_cb):
+        self._group_name = group_name
+        self._friend = friend
+        self._friend_keys = friend_keys
+        self._counter = counter
+        self._total = total
+        self._proceed_cb = proceed_cb
+
+    def _get_group_name(self):
+        return self._group_name
+
+    def _get_friend_keys(self):
+        return self._friend_keys
+
+    def _get_counter(self):
+        return self._counter
+
+    def _get_total(self):
+        return self._total
+
+    def _get_proceed_cb(self):
+        return self._proceed_cb
+
+    def _set_operation_status(self, status):
+        friends_model._set_last_operation_status_of_friend_in_group(self._group_name,
+                                                                    self._friend.get_key(),
+                                                                    status)
+
 
 class ObjectPalette(Palette):
 
@@ -117,6 +150,10 @@ class ObjectPalette(Palette):
         friends_menu.connect('friend-selected', self.__friend_selected_cb)
         menu_item.set_submenu(friends_menu)
 
+        groups_menu = GroupsMenu()
+        groups_menu.connect('group-selected', self.__group_selected_cb)
+        friends_menu._set_group_menu(groups_menu)
+
         if detail == True:
             menu_item = MenuItem(_('View Details'), 'go-right')
             menu_item.connect('activate', self.__detail_activate_cb)
@@ -150,7 +187,8 @@ class ObjectPalette(Palette):
     def __volume_error_cb(self, menu_item, message, severity):
         self.emit('volume-error', message, severity)
 
-    def __friend_selected_cb(self, menu_item, buddy):
+    def __friend_selected_cb(self, menu_item, buddy,
+            bulk_operation_details=None):
         logging.debug('__friend_selected_cb')
         file_name = model.get_file(self._metadata['uid'])
 
@@ -167,9 +205,51 @@ class ObjectPalette(Palette):
 
         if not mime_type:
             mime_type = mime.get_for_file(file_name)
-
         filetransfer.start_transfer(buddy, file_name, title, description,
-                                    mime_type)
+                                    mime_type, bulk_operation_details)
+
+    def __group_selected_cb(self, menu_item, group_name):
+        logging.debug('__group_selected_cb')
+        if group_name is not None:
+            friends_model._set_last_group_operation(group_name,
+                    _('(TRANSFER) %s') % (self._metadata['title'],))
+            friends_model._set_last_operation_status_of_friends_in_group_with_common_status(
+                    group_name, _('PENDING ...'))
+            friend_keys = \
+                    friends_model._get_friend_keys_of_group(group_name)
+
+            self._proceed_with_next_friend(group_name, friend_keys, 0)
+
+    """
+    This is the (callback) function that needs to be called per friend.
+    Note that this function is a callback (and not a looped one), since
+    the "next" friend iteration begins, only when the current iteration
+    has finished - which is asynchronous.
+    """
+    def _proceed_with_next_friend(self, group_name, friend_keys, counter):
+        counter = counter + 1
+
+        if counter <= len(friend_keys):
+            friend_key = friend_keys[counter-1]
+            friend = friends_model._get_friend_by_key(friend_key)
+
+            bulk_operation_details = \
+                    BulkOperationDetails(group_name,
+                                         friend,
+                                         friend_keys,
+                                         len(friend_keys),
+                                         counter,
+                                         self._proceed_with_next_friend)
+
+            # Only proceed if the friend is online.
+            # Else, set the failure-status, and move forward.
+            if friend.is_present():
+                self.__friend_selected_cb(None, friend, bulk_operation_details)
+            else:
+                bulk_operation_details._set_operation_status(_('FAILURE:\tFriend'
+                                                           ' is offline.'))
+                self._proceed_with_next_friend(group_name, friend_keys,
+                                               counter)
 
 
 class CopyMenu(gtk.Menu):
@@ -295,6 +375,41 @@ class ClipboardMenu(MenuItem):
         self._temp_file_path = None
 
 
+class GroupsMenu(gtk.Menu):
+    __gtype_name__ = 'GroupsMenu'
+
+    __gsignals__ = {
+        'group-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+                            ([object])),
+    }
+
+    def __init__(self):
+        gobject.GObject.__init__(self)
+
+        if filetransfer.file_transfer_available():
+            for group in friends_model._get_groups():
+                menu_item = MenuItem(text_label=group,
+                                     icon_name='zoom-groups')
+                menu_item.connect('activate', self.__item_activate_cb,
+                                  group)
+                self.append(menu_item)
+                menu_item.show()
+
+            if not self.get_children():
+                menu_item = MenuItem(_('No groups present'))
+                menu_item.set_sensitive(False)
+                self.append(menu_item)
+                menu_item.show()
+        else:
+            menu_item = MenuItem(_('No valid connection found'))
+            menu_item.set_sensitive(False)
+            self.append(menu_item)
+            menu_item.show()
+
+    def __item_activate_cb(self, menu_item, group):
+        self.emit('group-selected', group)
+
+
 class FriendsMenu(gtk.Menu):
     __gtype_name__ = 'JournalFriendsMenu'
 
@@ -307,7 +422,6 @@ class FriendsMenu(gtk.Menu):
         gobject.GObject.__init__(self)
 
         if filetransfer.file_transfer_available():
-            friends_model = friends.get_model()
             for friend in friends_model:
                 if friend.is_present():
                     menu_item = MenuItem(text_label=friend.get_nick(),
@@ -332,6 +446,12 @@ class FriendsMenu(gtk.Menu):
     def __item_activate_cb(self, menu_item, friend):
         self.emit('friend-selected', friend)
 
+    def _set_group_menu(self, group_menu):
+        menu_item = MenuItem(_('Select a group, to send'))
+        menu_item.set_submenu(group_menu)
+        self.append(menu_item)
+        menu_item.show()
+
 
 class StartWithMenu(gtk.Menu):
     __gtype_name__ = 'JournalStartWithMenu'
diff --git a/src/jarabe/model/buddy.py b/src/jarabe/model/buddy.py
index 8f17d7e..c6c6a4c 100644
--- a/src/jarabe/model/buddy.py
+++ b/src/jarabe/model/buddy.py
@@ -43,6 +43,7 @@ class BaseBuddyModel(gobject.GObject):
         self._color = None
         self._tags = None
         self._current_activity = None
+        self._groups = None
 
         gobject.GObject.__init__(self, **kwargs)
 
@@ -87,6 +88,16 @@ class BaseBuddyModel(gobject.GObject):
                                         getter=get_current_activity,
                                         setter=set_current_activity)
 
+    def get_groups(self):
+        return self._groups
+
+    def set_groups(self, groups):
+        self._groups = groups
+
+    groups = gobject.property(type=object,
+                              getter=get_groups,
+                              setter=set_groups)
+
     def is_owner(self):
         raise NotImplementedError
 
@@ -179,6 +190,7 @@ class BuddyModel(BaseBuddyModel):
         self._account = None
         self._contact_id = None
         self._handle = None
+        self._groups_list = []
 
         BaseBuddyModel.__init__(self, **kwargs)
 
diff --git a/src/jarabe/model/filetransfer.py b/src/jarabe/model/filetransfer.py
index 447a74a..3de8c20 100644
--- a/src/jarabe/model/filetransfer.py
+++ b/src/jarabe/model/filetransfer.py
@@ -124,6 +124,7 @@ class BaseFileTransfer(gobject.GObject):
         self.mime_type = None
         self.initial_offset = 0
         self.reason_last_change = FT_REASON_NONE
+        self._bulk_operation_details = None
 
     def set_channel(self, channel):
         self.channel = channel
@@ -156,6 +157,12 @@ class BaseFileTransfer(gobject.GObject):
     def _get_transferred_bytes(self):
         return self._transferred_bytes
 
+    def _get_bulk_operation_details(self):
+        return self._bulk_operation_details
+
+    def _set_bulk_operation_details(self, bulk_operation_details):
+        self._bulk_operation_details = bulk_operation_details
+
     transferred_bytes = gobject.property(type=int, default=0,
             getter=_get_transferred_bytes, setter=_set_transferred_bytes)
 
@@ -226,7 +233,8 @@ class IncomingFileTransfer(BaseFileTransfer):
 
 
 class OutgoingFileTransfer(BaseFileTransfer):
-    def __init__(self, buddy, file_name, title, description, mime_type):
+    def __init__(self, buddy, file_name, title, description, mime_type,
+            bulk_operation_details):
 
         presence_service = presenceservice.get_instance()
         name, path = presence_service.get_preferred_connection()
@@ -241,6 +249,7 @@ class OutgoingFileTransfer(BaseFileTransfer):
         self._socket = None
         self._splicer = None
         self._output_stream = None
+        self._set_bulk_operation_details(bulk_operation_details)
 
         self.buddy = buddy
         self.title = title
@@ -324,9 +333,12 @@ def init():
         _monitor_connection(connection)
 
 
-def start_transfer(buddy, file_name, title, description, mime_type):
+def start_transfer(buddy, file_name, title, description, mime_type,
+                   bulk_operation_details):
     outgoing_file_transfer = OutgoingFileTransfer(buddy, file_name, title,
-                                                  description, mime_type)
+                                                  description,
+                                                  mime_type,
+                                                  bulk_operation_details)
     new_file_transfer.send(None, file_transfer=outgoing_file_transfer)
 
 
diff --git a/src/jarabe/model/friends.py b/src/jarabe/model/friends.py
index 7605af1..448a36e 100644
--- a/src/jarabe/model/friends.py
+++ b/src/jarabe/model/friends.py
@@ -14,6 +14,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 gettext import gettext as _
+
 import os
 import logging
 from ConfigParser import ConfigParser
@@ -22,6 +24,7 @@ import gobject
 import dbus
 
 from sugar import env
+from sugar.util import unique_id
 from sugar.graphics.xocolor import XoColor
 
 from jarabe.model.buddy import BuddyModel
@@ -36,11 +39,11 @@ class FriendBuddyModel(BuddyModel):
 
     _NOT_PRESENT_COLOR = '#D5D5D5,#FFFFFF'
 
-    def __init__(self, nick, key, account=None, contact_id=None):
+    def __init__(self, nick, key, account=None, contact_id=None, groups=[]):
         self._online_buddy = None
 
         BuddyModel.__init__(self, nick=nick, key=key, account=account,
-                            contact_id=contact_id)
+                            contact_id=contact_id, groups=groups)
 
         neighborhood_model = neighborhood.get_model()
         neighborhood_model.connect('buddy-added', self.__buddy_added_cb)
@@ -110,35 +113,239 @@ class Friends(gobject.GObject):
 
     def __init__(self):
         gobject.GObject.__init__(self)
-
-        self._friends = {}
         self._path = os.path.join(env.get_profile_path(), 'friends')
+        self._groups_path = os.path.join(env.get_profile_path(), 'groups')
+        self.reinit()
 
+    def reinit(self):
+        self._friends = {}
+        self._groups = {}
         self.load()
 
     def has_buddy(self, buddy):
-        return buddy.get_key() in self._friends
+        return self.check_buddy_existence_by_key(buddy.get_key())
+
+    """
+    Ideally, this should be the only publically exposed API, since
+    only the buddy_key is the deciding factor of the existence of
+    a friend. There can never be two friends of the same key.
+    """
+    def check_buddy_existence_by_key(self, buddy_key):
+        return buddy_key in self._friends
 
     def add_friend(self, buddy_info):
         self._friends[buddy_info.get_key()] = buddy_info
         self.emit('friend-added', buddy_info)
 
-    def make_friend(self, buddy):
-        if not self.has_buddy(buddy):
-            buddy = FriendBuddyModel(key=buddy.key, nick=buddy.nick,
-                                     account=buddy.account,
-                                     contact_id=buddy.contact_id)
+    def add_friend_to_group(self, buddy_key, group_name,
+                            save_group=True):
+        if not self.__group_exists(group_name):
+            return _('Cannot add to group !! Create this group first !!')
+
+        if self.__friend_exists_in_group(group_name, buddy_key):
+            return _('Friend already exists in this group')
+
+        # If we reach here, it is safe to add this buddy :)
+        self._groups[group_name]['friends'][buddy_key] = {}
+
+        # Also, update the association from the buddy side, and save.
+        groups = self._get_groups_of_a_friend(buddy_key)
+        if group_name not in groups:
+            groups.append(group_name)
+            groups.sort()
+            self.save()
+
+        if save_group:
+            self.save_groups()
+
+    def remove_friend_group_assoc_using_friend_key(self, group_name,
+            friend_key, save_friend=True, save_group=True):
+        if not self.__group_exists(group_name):
+            return _('No such group exists !!')
+
+        # Remove association from friend.
+        if group_name in self._friends[friend_key].get_groups():
+            self._friends[friend_key].get_groups().remove(group_name)
+
+        # Remove association from group.
+        if friend_key in self._groups[group_name]['friends'].keys():
+            del self._groups[group_name]['friends'][friend_key]
+
+        if save_friend:
+            self.save()
+        if save_group:
+            self.save_groups()
+
+    def _get_groups_of_a_friend(self, friend_key):
+        if friend_key not in self._friends.keys():
+            return []
+
+        return self._friends[friend_key].get_groups()
+
+    def add_group(self, group_name, save_group=True):
+        if self.__group_exists(group_name):
+            return _('A group with the same name already exists !!'
+                     'Choose a different group-name.')
+
+        # If we reach here, it is safe to add a group of this name.
+        self._groups[group_name] = {}
+        self._groups[group_name]['friends'] = {}
+        self._groups[group_name]['last-operation'] = 'NONE.'
+        if save_group:
+            self.save_groups()
+
+    def remove_group(self, group_name, save_friend=True,
+            save_group=True):
+        if not self.__group_exists(group_name):
+            return _('No group exists of this name !!')
+
+        # Remove this group, from all the friend-associations.
+        for friend_key in self._friends.keys():
+            friend_groups_list = self._friends[friend_key].get_groups()
+            if group_name in friend_groups_list:
+                friend_groups_list.remove(group_name)
+        if save_friend:
+            self.save()
+
+        # Remove the group itself.
+        del self._groups[group_name]
+        if save_group:
+            self.save_groups()
+
+    def __group_exists(self, group_name):
+        if group_name in self._groups.keys():
+            return True
+        return False
+
+    def __friend_exists_in_group(self, group_name, friend_key):
+        if friend_key in \
+                self._groups[group_name]['friends'].keys():
+            return True
+        return False
+
+    """
+    This method is useful in the case, when the buddy is to be added as
+    a friend in deferred sense. For example, the buddies list may first be
+    fetched by virtue of their being online, but may go offline by the
+    time they are about to be saved. So, in that case, we retrieve the
+    parameters that are required for saving in the early stages itself.
+    """
+    def make_friend_by_parameters(self, buddy_key, buddy_nick, buddy_account, buddy_contact_id):
+        if not self.check_buddy_existence_by_key(buddy_key):
+            buddy = FriendBuddyModel(key=buddy_key,
+                                     nick=buddy_nick,
+                                     account=buddy_account,
+                                     contact_id=buddy_contact_id)
             self.add_friend(buddy)
             self.save()
 
+    def make_friend(self, buddy):
+        self.make_friend_by_parameters(buddy.key, buddy.nick,
+                                       buddy.account, buddy.contact_id)
+
+    def remove_groups_of_friend(self, buddy_info, save_friend=True,
+            save_group=True):
+        groups = self._get_groups_of_a_friend(buddy_info.get_key())
+        for group in groups:
+            self.remove_friend_group_assoc_using_friend_key(group,
+                    buddy_info.get_key(), False, False)
+
+        if save_friend:
+            self.save()
+        if save_group:
+            self.save_groups()
+
     def remove(self, buddy_info):
+        # First remove its association from all its groups
+        self.remove_groups_of_friend(buddy_info, False, True)
+
+        # Now, remove the friend-entity itself.
         del self._friends[buddy_info.get_key()]
         self.save()
+
         self.emit('friend-removed', buddy_info.get_key())
 
     def __iter__(self):
         return self._friends.values().__iter__()
 
+    def _get_friend_by_key(self, key):
+        if not (key in self._friends.keys()):
+            return _('No friend found !!')
+        return self._friends[key]
+
+    def _get_friend_keys_of_group(self, group_name):
+        if not self.__group_exists(group_name):
+            return []
+
+        return self._groups[group_name]['friends'].keys()
+
+    def _get_groups(self):
+        # Sascha's wonderful feedback: always export only the thing
+        # required. Here, we required only the group-names; so export
+        # just the keys
+        return self._groups.keys()
+
+    def _set_groups(self, groups):
+        self._groups = groups
+
+    def _get_group_by_key_name(self, group_name):
+        if not self.__group_exists(group_name):
+            return None
+        return self._groups[group_name]
+
+    def _get_last_group_operation(self, group_name):
+        if not self.__group_exists(group_name):
+            return _('No group of this name exists !!')
+        return self._groups[group_name]['last-operation']
+
+    def _set_last_group_operation(self, group_name, operation):
+        if not self.__group_exists(group_name):
+            return _('No group exists.')
+        self._groups[group_name]['last-operation'] = operation
+        self.save_groups
+
+    def _get_last_operation_status_of_friend_in_group(self, group_name,
+                                                      friend_key):
+        if not self.__group_exists(group_name):
+            return _('No group exists.')
+
+        if not self.__friend_exists_in_group(group_name, friend_key):
+            return _('Friend does not exist in group')
+
+        if 'last-operation-status' in \
+                self._groups[group_name]['friends'][friend_key].keys():
+            return self._groups[group_name]['friends'][friend_key]['last-operation-status']
+
+    def _set_last_operation_status_of_friend_in_group(self, group_name,
+                                                      friend_key, status,
+                                                      save_group=True):
+        if not self.__group_exists(group_name):
+            return _('No group exists.')
+
+        if not self.__friend_exists_in_group(group_name, friend_key):
+            return _('Friend does not exist in group')
+
+        self._groups[group_name]['friends'][friend_key]['last-operation-status'] = \
+                status
+
+        if save_group:
+            self.save_groups()
+
+    def _set_last_operation_status_of_friends_in_group_with_common_status(
+            self, group_name, status):
+        if not self.__group_exists(group_name):
+            return _('No group exists.')
+
+        friend_keys = self._get_friend_keys_of_group(group_name)
+        for friend_key in friend_keys:
+            self._set_last_operation_status_of_friend_in_group(group_name,
+                                                               friend_key,
+                                                               status,
+                                                               False)
+
+        # Save the groups in one go.
+        self.save_groups()
+
     def load(self):
         cp = ConfigParser()
 
@@ -149,11 +356,36 @@ class Friends(gobject.GObject):
                     # HACK: don't screw up on old friends files
                     if len(key) < 20:
                         continue
-                    buddy = FriendBuddyModel(key=key, nick=cp.get(key, 'nick'))
+
+                    # Check for the existence of 'groups' option (for
+                    # backwards compatability)
+                    groups = []
+                    if cp.has_option(key, 'groups'):
+                        groups = eval(cp.get(key, 'groups'))
+
+                    buddy = FriendBuddyModel(key=key, nick=cp.get(key,
+                        'nick'), groups=groups)
                     self.add_friend(buddy)
         except Exception:
             logging.exception('Error parsing friends file')
 
+        self.__load_groups()
+
+    def __load_groups(self):
+        cp = ConfigParser()
+
+        try:
+            success = cp.read([self._groups_path])
+            if success:
+                for group_name in cp.sections():
+                    self._groups[group_name] = {}
+                    self._groups[group_name]['friends'] = \
+                            eval(cp.get(group_name, 'friends'))
+                    self._groups[group_name]['last-operation'] = \
+                            cp.get(group_name, 'last-operation')
+        except:
+            logging.exception('Error while loading config')
+
     def save(self):
         cp = ConfigParser()
 
@@ -161,11 +393,24 @@ class Friends(gobject.GObject):
             section = friend.get_key()
             cp.add_section(section)
             cp.set(section, 'nick', friend.get_nick())
+            cp.set(section, 'groups', friend.get_groups())
 
         fileobject = open(self._path, 'w')
         cp.write(fileobject)
         fileobject.close()
 
+    def save_groups(self):
+        cp = ConfigParser()
+
+        for group in self._groups.keys():
+            cp.add_section(group)
+            cp.set(group, 'friends', self._groups[group]['friends'])
+            cp.set(group, 'last-operation',
+                    self._groups[group]['last-operation'])
+
+        fileobject = open(self._groups_path, 'w')
+        cp.write(fileobject)
+        fileobject.close()
 
 def get_model():
     global _model
diff --git a/src/jarabe/view/buddymenu.py b/src/jarabe/view/buddymenu.py
index de5a772..544faeb 100644
--- a/src/jarabe/view/buddymenu.py
+++ b/src/jarabe/view/buddymenu.py
@@ -22,6 +22,7 @@ import gtk
 import gconf
 import glib
 import dbus
+import gobject
 
 from sugar.graphics.palette import Palette
 from sugar.graphics.menuitem import MenuItem
@@ -33,6 +34,34 @@ from jarabe.model.session import get_session_manager
 from jarabe.controlpanel.gui import ControlPanel
 import jarabe.desktop.homewindow
 
+friends_model = friends.get_model()
+
+class GroupsMenu(gtk.Menu):
+    __gtype_name__ = 'NeighbourhoodGroupsMenu'
+
+    __gsignals__ = {
+        'group-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+                            ([object])),
+    }
+
+    def __init__(self, groups, no_groups_label):
+        gobject.GObject.__init__(self)
+
+        for group in groups:
+            menu_item = MenuItem(text_label=group, icon_name='zoom-groups')
+            menu_item.connect('activate', self.__item_activate_cb, group)
+            self.append(menu_item)
+            menu_item.show()
+
+        if not self.get_children():
+            menu_item = MenuItem(no_groups_label)
+            menu_item.set_sensitive(False)
+            self.append(menu_item)
+            menu_item.show()
+
+    def __item_activate_cb(self, menu_item, group):
+        self.emit('group-selected', group)
+
 
 class BuddyMenu(Palette):
     def __init__(self, buddy):
@@ -64,19 +93,57 @@ class BuddyMenu(Palette):
 
     def _add_buddy_items(self):
         if friends.get_model().has_buddy(self._buddy):
-            menu_item = MenuItem(_('Remove friend'), 'list-remove')
+            menu_item = MenuItem(_('Remove friend, and its association'
+                                   ' from all groups'), 'list-remove')
             menu_item.connect('activate', self._remove_friend_cb)
+            self.menu.append(menu_item)
         else:
-            menu_item = MenuItem(_('Make friend'), 'list-add')
+            menu_item = MenuItem(_('Make friend (without associating'
+                                   ' to any group)'), 'list-add')
             menu_item.connect('activate', self._make_friend_cb)
-
-        self.menu.append(menu_item)
-        menu_item.show()
+            self.menu.append(menu_item)
+
+        menu_item_2 = MenuItem(_('Make friend (if not already), and add to group'),
+                'list-add')
+
+        all_groups = friends_model._get_groups()
+        groups_of_friend = \
+                friends_model._get_groups_of_a_friend(self._buddy.get_key())
+
+        # Make a local copy of groups, of which the friend is not
+        # associated with.
+        groups_not_of_friend = []
+        for group in all_groups:
+            groups_not_of_friend.append(group)
+        for group in groups_of_friend:
+            groups_not_of_friend.remove(group)
+
+        groups_of_friend.sort()
+        groups_not_of_friend.sort()
+
+        add_groups_menu = GroupsMenu(groups_not_of_friend, _('No groups to'
+                                                             ' associate'))
+        add_groups_menu.connect('group-selected',
+                self.__make_friend_and_add_to_group)
+        self.menu.append(menu_item_2)
+        menu_item_2.set_submenu(add_groups_menu)
+        add_groups_menu.show()
+
+        menu_item_3 = MenuItem(_('Remove friend from group'), 'list-remove')
+        remove_groups_menu = GroupsMenu(groups_of_friend, _('No groups to'
+                                                            ' disassociate'))
+        remove_groups_menu.connect('group-selected',
+                self.__remove_friend_from_group)
+        self.menu.append(menu_item_3)
+        menu_item_3.set_submenu(remove_groups_menu)
+        remove_groups_menu.show()
 
         self._invite_menu = MenuItem('')
         self._invite_menu.connect('activate', self._invite_friend_cb)
         self.menu.append(self._invite_menu)
 
+        self.menu.show_all()
+
         home_model = shell.get_model()
         self._active_activity_changed_hid = home_model.connect(
                 'active-activity-changed', self._cur_activity_changed_cb)
@@ -154,7 +221,7 @@ class BuddyMenu(Palette):
     def __buddy_notify_nick_cb(self, buddy, pspec):
         self.set_primary_text(glib.markup_escape_text(buddy.props.nick))
 
-    def _make_friend_cb(self, menuitem):
+    def _make_friend_cb(self, menuitem=None):
         friends.get_model().make_friend(self._buddy)
 
     def _remove_friend_cb(self, menuitem):
@@ -178,3 +245,11 @@ class BuddyMenu(Palette):
                     raise
         else:
             logging.error('Invite failed, activity service not ')
+
+    def __make_friend_and_add_to_group(self, menu_item, group):
+        self._make_friend_cb()
+        friends_model.add_friend_to_group(self._buddy.get_key(), group)
+
+    def __remove_friend_from_group(self, menu_item, group):
+        friends_model.remove_friend_group_assoc_using_friend_key(group,
+                self._buddy.get_key())
-- 
1.7.4.4



More information about the Sugar-devel mailing list