1 """
2 Command Line Interface to PISI - one implementation of user interaction
3
4 This file is part of Pisi.
5
6 This module provides the CLI interface to PISI:
7 - controlling the entire CLI application
8 - Checking of Arguments
9
10 A callback is defined as well, which handles all the output coming from the application core.
11
12 Pisi is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 3 of the License, or
15 (at your option) any later version.
16
17 Pisi is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
21
22 You should have received a copy of the GNU General Public License
23 along with Pisi. If not, see <http://www.gnu.org/licenses/>.
24 """
25 from pisiconstants import *
26 import pisiprogress
27 import pisi
28 import sys
29 import os
30 import ConfigParser
31 from thirdparty.epydocutil import TerminalController
32 import warnings
33
35 """
36 Command Line Interface to PISI
37 """
38
50
52 """
53 Output the string to console
54 """
55 if not self.isVerbose:
56 sys.stdout.write(self.term.CLEAR_LINE)
57 print st
58
60 """
61 Redirect to L{message}
62 """
63 self.message(st)
64
66 """
67 Only redirect to L{message} if we are in verbose mode
68 """
69 if self.isVerbose:
70 self.message(st)
71
73 """
74 Prepare a prompt and return the user input
75
76 If user input is empty, the provided default is returned.
77 """
78 if default:
79 prompt += "[" + default + "]"
80 st = raw_input(prompt + ": ")
81 if default:
82 if st == '':
83 return default
84 return st
85
87 """
88 Ask user for a single confirmation (OK / Cancel)
89 """
90 st = raw_input(prompt + " (Y/N): ")
91 return st.strip().upper() == 'Y'
92
94 """
95 If we are not in verbose mode, we display the progress here
96 """
97 if not self.isVerbose:
98 percent = self.progress.calculateOverallProgress() / 100.0
99 message = status
100 background = "." * CONSOLE_PROGRESSBAR_WIDTH
101 dots = int(len(background) * percent)
102 sys.stdout.write(self.term.CLEAR_LINE + '%3d%% '%(100*percent) + self.term.GREEN + '[' + self.term.BOLD + '='*dots + background[dots:] + self.term.NORMAL + self.term.GREEN + '] ' + self.term.NORMAL + message + self.term.BOL)
103 sys.stdout.flush()
104 if status == 'Finished':
105 print
106
107
109 """
110 Supporting function to print a string with a fixed length (good for tables)
111
112 Cuts the string when too long and fills up with characters when too short.
113 """
114 return (str + (spaces * width))[:width]
115
117 """
118 Gets around the problem if an attribute in one data source is not set in the other one at all
119
120 Returns default value if the attribute is not available in the given dictionery (key error).
121 """
122 try:
123 return dict[key]
124 except KeyError:
125 return default
126
127
129 """
130 Supporting function to show differences for two contact entries
131
132 Prints a table with all attributes being different in the two sources for comparing two contact entries.
133 """
134 diffList = pisi.determineConflictDetails(entry1, entry2)
135
136 print "\n " + "=" * 25 + " DETAILS " + "=" * 25
137 print "* %s, %s" %(entry1.attributes['lastname'],entry1.attributes['firstname'])
138 print self._strFixedLen('Attribute', 15) + " | " + self._strFixedLen(nameSource1, 20) + " | " + self._strFixedLen(nameSource2, 20)
139 print "-" * 60
140 for key in diffList.keys():
141 print self._strFixedLen(key, 15) + " | " + self._strFixedLen(self._getDetailValue(entry1.attributes, key), 20) + " | " + self._strFixedLen(self._getDetailValue(entry2.attributes, key), 20)
142 print "-" * 60
143
145 """
146 Use interaction for choosing which contact entry to keep in case of a conflict
147
148 Iterates through the given list of contact IDs and requests the user for choosing an action for every single entry.
149
150 @return: A dictionary which contains the action for each conflict entry; the key is the id of the contact entry,
151 the value one out of a - keep entry from first source, b - keep value from second source and s - skip this entry (no change on either side)
152 """
153 if len(idList) > 0:
154 print
155 print "Please resolve the following conflicts manually"
156 ret = {}
157 for entryID in idList:
158 entry1 = source[0].getContact(entryID)
159 entry2 = source[1].getContact(entryID)
160 myinput = ''
161 while myinput != 'a' and myinput != 'b' and myinput != 's':
162 if myinput != 'd':
163 print "* %s, %s" %(entry1.attributes['lastname'],entry1.attributes['firstname'])
164 else:
165 self._printConflictDetails(entry1, entry2, source[0].getName(), source[1].getName() )
166 print " [a]-Keep <%s> [b]-Keep <%s> [d]etails [s]kip [a|b|d|s]: " %(source[0].getName(), source[1].getName()),
167 myinput = raw_input()
168 ret[entryID] = myinput
169 return ret
170
172 """
173 Checks, whether configuration can be loaded from PISI core.
174
175 If not possible, an error message is printed and False will be returned.
176 @return: False, if an Error occurs when loading the configration from core; otherwise True
177 """
178 try:
179 pisi.getConfiguration()
180 return True
181 except ValueError:
182 print ("PISI configuration not found")
183 print ("For running PISI you must have a configuration file located in '/home/root/.pisi/conf'.\n\nWith the package a well-documented sample was placed at '/usr/share/doc/pisi/conf.example'. You may rename this for a starting point - then edit this file in order to configure your PIM synchronization data sources.")
184 return False
185
186
188 """
189 Controls the major flow for PISI (CLI)
190
191 Calls one after another the supporting functions in this module.
192 """
193 if not testConfiguration():
194 sys.exit(0)
195
196 verbose, modulesToLoad, modulesNamesCombined, soft, mergeMode = parseArguments()
197 cb = CLICallback(verbose)
198 pisiprogress.registerCallback(cb)
199
200 cb.progress.push(0, 8)
201 cb.update('Starting Configuration')
202 cb.verbose('')
203 cb.verbose("*" * 55)
204 cb.verbose( "*" * 22 + " PISI " + "*" * 22)
205 cb.verbose( "*" * 55)
206 cb.verbose( "** PISI is synchronizing information " + "*" * 18)
207 cb.verbose( "** http://freshmeat.net/projects/pisiom " + "*" * 8)
208 cb.verbose( "*" * 55)
209
210 cb.verbose( ("\n" + "*" * 15 + " PHASE 0 - Configuration " + "*" * 15))
211 cb.verbose( "Verbose mode on")
212 cb.verbose( ("In case of conflicts I use the following strategy: %s" %(MERGEMODE_STRINGS[mergeMode])))
213
214 config, configfolder = pisi.readConfiguration()
215 source = pisi.importModules(configfolder, config, modulesToLoad, modulesNamesCombined, soft)
216 mode = pisi.determineMode(config, modulesToLoad)
217 cb.progress.drop()
218
219 cb.progress.push(8, 10)
220 cb.update('Pre-Processing sources')
221 cb.verbose("\tSource 1")
222 source[0].preProcess()
223 cb.verbose("\tSource 2")
224 source[1].preProcess()
225 cb.verbose(" Pre-Processing Done")
226 cb.progress.drop()
227
228 cb.progress.push(10, 40)
229 cb.update('Loading from sources')
230 cb.verbose("\n" + "*" * 18 + " PHASE 1 - Loading " + "*" * 18)
231 cb.progress.push(0, 50)
232 source[0].load()
233 cb.progress.drop()
234 cb.progress.push(50, 100)
235 cb.update('Loading')
236 source[1].load()
237 cb.progress.drop()
238 cb.progress.drop()
239
240 cb.progress.push(40, 70)
241 cb.update('Comparing sources')
242 cb.verbose("\n" + "*" * 17 + " PHASE 2 - Comparing " + "*" * 17)
243 if mode == MODE_CALENDAR:
244 pisi.eventsSync.syncEvents(verbose, modulesToLoad, source)
245 elif mode == MODE_CONTACTS:
246 pisi.contactsSync.syncContacts(verbose, modulesToLoad, source, mergeMode)
247 cb.progress.drop()
248
249 cb.progress.push(70, 95)
250 cb.update('Making changes permanent')
251 cb.verbose ("\n" + "*" * 18 + " PHASE 3 - Saving " + "*" * 18)
252 if soft:
253 print "You chose soft mode for PISI - changes are not applied to data sources."
254 else:
255 pisi.applyChanges(source)
256 cb.verbose( "*" * 24 + " DONE " + "*" * 24)
257 cb.progress.drop()
258
259 cb.progress.push(95, 100)
260 cb.update('Post-Processing sources')
261 cb.verbose("\tSource 1")
262 source[0].postProcess()
263 cb.verbose("\tSource 2")
264 source[1].postProcess()
265 cb.verbose(" Post-Processing Done")
266 cb.progress.drop()
267
268 cb.update('Finished')
269
270
272 """
273 Parses command line arguments
274
275 All information from the command line arguments are returned by this function.
276 If the number of arguments given is not valid, a help text is printed on the console by calling function L{usage}.
277 """
278 mergeMode = MERGEMODE_SKIP
279 modulesToLoad = []
280 modulesNamesCombined = ""
281 soft = False
282 verbose = False
283 for arg in sys.argv[1:]:
284 if arg[:1]!='-':
285 modulesToLoad.append( arg )
286 modulesNamesCombined += arg
287 elif arg=='-v' or arg=='--verbose':
288 verbose=True
289 elif arg=='-s' or arg=='--soft':
290 soft = True
291 elif arg=='-l' or arg=='--list-configurations':
292 list_configurations()
293 elif arg.startswith('-m'):
294 mergeMode = int(arg[2:])
295 if len(modulesToLoad)!=2:
296 usage()
297 return verbose, modulesToLoad, modulesNamesCombined, soft, mergeMode
298
300 """
301 Prints a help text to the console
302
303 The application is shut down afterwards.
304 """
305 usage = """You start the program by specifying 2 sources to synchronize.
306 Like this:
307 ./pisi [options] $SOURCE1 $SOURCE2
308 Flags:
309 -v --verbose
310 Make program verbose
311 -s --soft
312 Don't actually make any changes on the servers/in the files
313 -l --list-configurations
314 List which configurations there are in the config-file (this don't need
315 the two modules)
316 -mX
317 Define the mode to deal with conflicts:
318 -m0 SKIP entry (default)
319 -m1 FLUSH source 1 in the beginning
320 -m2 FLUSH source 2 in the beginning
321 -m3 Overwrite source 1 entry wise
322 -m4 Overwrite source 2 entry wise
323 -m5 Manually confirm each entry
324
325 Configuration:
326 Read http://wiki.github.com/kichkasch/pisi/an-end-user for help.
327 """
328 sys.exit(usage)
329
331 """
332 Prints available configurations for data sources
333
334 Parses the configuration file for PISI and prints all available data sources to the console.
335 """
336 config = pisi.getConfiguration()
337 print 'You have these configurations:'
338 print ' [1] Contacts sources'
339 for con in config.sections():
340 if config.get(con,'module').startswith('contacts'):
341 print ('\t-%s which uses module <%s>: %s' %(con, config.get(con,'module'), config.get(con,'description')))
342 print ' [2] Calendar sources'
343 for con in config.sections():
344 if config.get(con,'module').startswith('calendar'):
345 print ('\t-%s which uses module <%s>: %s' %(con, config.get(con,'module'), config.get(con,'description')))
346 sys.exit(0)
347