Mercurial > libervia-backend
comparison sat_frontends/quick_frontend/quick_contact_list.py @ 2617:81b70eeb710f
quick_frontend(contact list): refactored update:
update is now called with appropriate constant value (C.UPDATE_ADD, C.UPDATE_DELETE, C.UPDATE_MODIFY and so on) when a widget change visibility according to current options.
Before it was linked to cache only (C.UPDATE_ADD was only called when contact was first added to cache).
This make widget handling in frontends more easy.
Renamed entityToShow to entityVisible, which seems to correspond better.
Started reducing lines lenght to 90 chars as a test. May become the new coding style soon.
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 24 Jun 2018 21:59:29 +0200 |
parents | 2e6864b1d577 |
children | 56f94936df1e |
comparison
equal
deleted
inserted
replaced
2616:1cc88adb5142 | 2617:81b70eeb710f |
---|---|
15 # GNU Affero General Public License for more details. | 15 # GNU Affero General Public License for more details. |
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 17 # You should have received a copy of the GNU Affero General Public License |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 """Contact List handling multi profiles at once, should replace quick_contact_list module in the future""" | 20 """Contact List handling multi profiles at once, |
21 should replace quick_contact_list module in the future""" | |
21 | 22 |
22 from sat.core.i18n import _ | 23 from sat.core.i18n import _ |
23 from sat.core.log import getLogger | 24 from sat.core.log import getLogger |
24 log = getLogger(__name__) | 25 log = getLogger(__name__) |
25 from sat.core import exceptions | 26 from sat.core import exceptions |
30 | 31 |
31 | 32 |
32 try: | 33 try: |
33 # FIXME: to be removed when an acceptable solution is here | 34 # FIXME: to be removed when an acceptable solution is here |
34 unicode('') # XXX: unicode doesn't exist in pyjamas | 35 unicode('') # XXX: unicode doesn't exist in pyjamas |
35 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options | 36 except (TypeError, AttributeError): # Error raised is not the same depending on |
37 # pyjsbuild options | |
36 # XXX: pyjamas' max doesn't support key argument, so we implement it ourself | 38 # XXX: pyjamas' max doesn't support key argument, so we implement it ourself |
37 pyjamas_max = max | 39 pyjamas_max = max |
38 | 40 |
39 def max(iterable, key): | 41 def max(iterable, key): |
40 iter_cpy = list(iterable) | 42 iter_cpy = list(iterable) |
86 # options | 88 # options |
87 self.show_disconnected = False | 89 self.show_disconnected = False |
88 self.show_empty_groups = True | 90 self.show_empty_groups = True |
89 self.show_resources = False | 91 self.show_resources = False |
90 self.show_status = False | 92 self.show_status = False |
91 | 93 # do we show entities with notifications? |
92 self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self._showEmptyGroups) | 94 # if True, entities will be show even if they normally would not |
93 self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self._showOfflineContacts) | 95 # (e.g. not in contact list) if they have notifications attached |
94 | 96 self.show_entities_with_notifs = True |
95 # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) | 97 |
98 self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, | |
99 "General", | |
100 profile_key=profile, | |
101 callback=self._showEmptyGroups) | |
102 | |
103 self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, | |
104 "General", | |
105 profile_key=profile, | |
106 callback=self._showOfflineContacts) | |
107 | |
108 # FIXME: workaround for a pyjamas issue: calling hash on a class method always | |
109 # return a different value if that method is defined directly within the | |
110 # class (with the "def" keyword) | |
96 self.presenceListener = self.onPresenceUpdate | 111 self.presenceListener = self.onPresenceUpdate |
97 self.host.addListener('presence', self.presenceListener, [self.profile]) | 112 self.host.addListener('presence', self.presenceListener, [self.profile]) |
98 self.nickListener = self.onNickUpdate | 113 self.nickListener = self.onNickUpdate |
99 self.host.addListener('nick', self.nickListener, [self.profile]) | 114 self.host.addListener('nick', self.nickListener, [self.profile]) |
100 self.notifListener = self.onNotification | 115 self.notifListener = self.onNotification |
114 | 129 |
115 def __contains__(self, entity): | 130 def __contains__(self, entity): |
116 """Check if entity is in contact list | 131 """Check if entity is in contact list |
117 | 132 |
118 An entity can be in contact list even if not in roster | 133 An entity can be in contact list even if not in roster |
119 @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed) | 134 @param entity (jid.JID): jid of the entity (resource is not ignored, |
135 use bare jid if needed) | |
120 """ | 136 """ |
121 if entity.resource: | 137 if entity.resource: |
122 try: | 138 try: |
123 return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES) | 139 return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES) |
124 except KeyError: | 140 except exceptions.NotFound: |
125 return False | 141 return False |
126 return entity in self._cache | 142 return entity in self._cache |
127 | 143 |
128 @property | 144 @property |
129 def roster(self): | 145 def roster(self): |
137 def roster_connected(self): | 153 def roster_connected(self): |
138 """Return all the bare JIDs of the roster entities that are connected. | 154 """Return all the bare JIDs of the roster entities that are connected. |
139 | 155 |
140 @return (set[jid.JID]) | 156 @return (set[jid.JID]) |
141 """ | 157 """ |
142 return set([entity for entity in self._roster if self.getCache(entity, C.PRESENCE_SHOW) is not None]) | 158 return set([entity for entity in self._roster |
159 if self.getCache(entity, C.PRESENCE_SHOW) is not None]) | |
143 | 160 |
144 @property | 161 @property |
145 def roster_entities_by_group(self): | 162 def roster_entities_by_group(self): |
146 """Return a dictionary binding the roster groups to their entities bare JIDs. | 163 """Return a dictionary binding the roster groups to their entities bare JIDs. |
147 | 164 |
184 """Return item representation for all visible entities in cache | 201 """Return item representation for all visible entities in cache |
185 | 202 |
186 entities are not sorted | 203 entities are not sorted |
187 key: bare jid, value: data | 204 key: bare jid, value: data |
188 """ | 205 """ |
189 return {jid_:cache for jid_, cache in self._cache.iteritems() if self.entityToShow(jid_)} | 206 return {jid_:cache for jid_, cache in self._cache.iteritems() |
207 if self.entityVisible(jid_)} | |
190 | 208 |
191 | 209 |
192 def getItem(self, entity): | 210 def getItem(self, entity): |
193 """Return item representation of requested entity | 211 """Return item representation of requested entity |
194 | 212 |
196 @raise (KeyError): entity is unknown | 214 @raise (KeyError): entity is unknown |
197 """ | 215 """ |
198 return self._cache[entity] | 216 return self._cache[entity] |
199 | 217 |
200 def _gotContacts(self, contacts): | 218 def _gotContacts(self, contacts): |
201 """Called during filling, add contacts and notice parent that contacts are filled""" | 219 """Add contacts and notice parent that contacts are filled |
220 | |
221 Called during initial contact list filling | |
222 @param contacts(tuple): all contacts | |
223 """ | |
202 for contact in contacts: | 224 for contact in contacts: |
203 self.host.newContactHandler(*contact, profile=self.profile) | 225 self.host.newContactHandler(*contact, profile=self.profile) |
204 handler._contactsFilled(self.profile) | 226 handler._contactsFilled(self.profile) |
205 | 227 |
206 def _fill(self): | 228 def _fill(self): |
212 self.host.bridge.getContacts(self.profile, callback=self._gotContacts) | 234 self.host.bridge.getContacts(self.profile, callback=self._gotContacts) |
213 | 235 |
214 def fill(self): | 236 def fill(self): |
215 handler.fill(self.profile) | 237 handler.fill(self.profile) |
216 | 238 |
217 def getCache(self, entity, name=None, bare_default=True): | 239 def getCache(self, entity, name=None, bare_default=True, create_if_not_found=False): |
218 """Return a cache value for a contact | 240 """Return a cache value for a contact |
219 | 241 |
220 @param entity(jid.JID): entity of the contact from who we want data (resource is used if given) | 242 @param entity(jid.JID): entity of the contact from who we want data |
243 (resource is used if given) | |
221 if a resource specific information is requested: | 244 if a resource specific information is requested: |
222 - if no resource is given (bare jid), the main resource is used, according to priority | 245 - if no resource is given (bare jid), the main resource is used, |
246 according to priority | |
223 - if resource is given, it is used | 247 - if resource is given, it is used |
224 @param name(unicode): name the data to get, or None to get everything | 248 @param name(unicode): name the data to get, or None to get everything |
225 @param bare_default(bool, None): if True and entity is a full jid, the value of bare jid | 249 @param bare_default(bool, None): if True and entity is a full jid, |
226 will be returned if not value is found for the requested resource. | 250 the value of bare jid will be returned if not value is found for |
251 the requested resource. | |
227 If False, None is returned if no value is found for the requested resource. | 252 If False, None is returned if no value is found for the requested resource. |
228 If None, bare_default will be set to False if entity is in a room, True else | 253 If None, bare_default will be set to False if entity is in a room, True else |
254 @param create_if_not_found(bool): if True, create contact if it's not found | |
255 in cache | |
229 @return: full cache if no name is given, or value of "name", or None | 256 @return: full cache if no name is given, or value of "name", or None |
257 @raise NotFound: entity not found in cache | |
230 """ | 258 """ |
231 # FIXME: resource handling need to be reworked | 259 # FIXME: resource handling need to be reworked |
232 # FIXME: bare_default work for requesting full jid to get bare jid, but not the other way | 260 # FIXME: bare_default work for requesting full jid to get bare jid, |
233 # e.g.: if we have set an avatar for user@server.tld/resource and we request user@server.tld | 261 # but not the other way |
262 # e.g.: if we have set an avatar for user@server.tld/resource | |
263 # and we request user@server.tld | |
234 # we won't get the avatar set in the resource | 264 # we won't get the avatar set in the resource |
235 try: | 265 try: |
236 cache = self._cache[entity.bare] | 266 cache = self._cache[entity.bare] |
237 except KeyError: | 267 except KeyError: |
238 self.setContact(entity) | 268 if create_if_not_found: |
239 cache = self._cache[entity.bare] | 269 self.setContact(entity) |
270 cache = self._cache[entity.bare] | |
271 else: | |
272 raise exceptions.NotFound | |
240 | 273 |
241 if name is None: | 274 if name is None: |
275 # full cache is requested | |
242 return cache | 276 return cache |
243 | 277 |
244 if name in ('status', C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW): | 278 if name in ('status', C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW): |
245 # these data are related to the resource | 279 # these data are related to the resource |
246 if not entity.resource: | 280 if not entity.resource: |
335 def setSpecial(self, entity, special_type): | 369 def setSpecial(self, entity, special_type): |
336 """Set special flag on an entity | 370 """Set special flag on an entity |
337 | 371 |
338 @param entity(jid.JID): jid of the special entity | 372 @param entity(jid.JID): jid of the special entity |
339 if the jid is full, will be added to special extras | 373 if the jid is full, will be added to special extras |
340 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to remove special flag | 374 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) |
375 or None to remove special flag | |
341 """ | 376 """ |
342 assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,) | 377 assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,) |
343 self.setCache(entity, C.CONTACT_SPECIAL, special_type) | 378 self.setCache(entity, C.CONTACT_SPECIAL, special_type) |
344 | 379 |
345 def getSpecials(self, special_type=None, bare=False): | 380 def getSpecials(self, special_type=None, bare=False): |
346 """Return all the bare JIDs of the special roster entities of with given type. | 381 """Return all the bare JIDs of the special roster entities of with given type. |
347 | 382 |
348 @param special_type(unicode, None): if not None, filter by special type (e.g. C.CONTACT_SPECIAL_GROUP) | 383 @param special_type(unicode, None): if not None, filter by special type |
384 (e.g. C.CONTACT_SPECIAL_GROUP) | |
349 @param bare(bool): return only bare jids if True | 385 @param bare(bool): return only bare jids if True |
350 @return (iter[jid.JID]): found special entities | 386 @return (iter[jid.JID]): found special entities |
351 """ | 387 """ |
352 for entity in self._specials: | 388 for entity in self._specials: |
353 if bare and entity.resource: | 389 if bare and entity.resource: |
354 continue | 390 continue |
355 if special_type is not None and self.getCache(entity, C.CONTACT_SPECIAL) != special_type: | 391 if (special_type is not None |
392 and self.getCache(entity, C.CONTACT_SPECIAL) != special_type): | |
356 continue | 393 continue |
357 yield entity | 394 yield entity |
358 | 395 |
359 def disconnect(self): | 396 def disconnect(self): |
360 # for now we just clear contacts on disconnect | 397 # for now we just clear contacts on disconnect |
379 This method can be called with groups=None for the purpose of updating | 416 This method can be called with groups=None for the purpose of updating |
380 the contact's attributes (e.g. nickname). In that case, the groups | 417 the contact's attributes (e.g. nickname). In that case, the groups |
381 attribute must not be set to the default group but ignored. If not, | 418 attribute must not be set to the default group but ignored. If not, |
382 you may move your contact from its actual group(s) to the default one. | 419 you may move your contact from its actual group(s) to the default one. |
383 | 420 |
384 None value for 'groups' has a different meaning than [None] which is for the default group. | 421 None value for 'groups' has a different meaning than [None] |
422 which is for the default group. | |
385 | 423 |
386 @param entity (jid.JID): entity to add or replace | 424 @param entity (jid.JID): entity to add or replace |
387 if entity is a full jid, attributes will be cached in for the full jid only | 425 if entity is a full jid, attributes will be cached in for the full jid only |
388 @param groups (list): list of groups or None to ignore the groups membership. | 426 @param groups (list): list of groups or None to ignore the groups membership. |
389 @param attributes (dict): attibutes of the added jid or to update | 427 @param attributes (dict): attibutes of the added jid or to update |
392 """ | 430 """ |
393 if attributes is None: | 431 if attributes is None: |
394 attributes = {} | 432 attributes = {} |
395 | 433 |
396 entity_bare = entity.bare | 434 entity_bare = entity.bare |
397 update_type = C.UPDATE_MODIFY if entity_bare in self._cache else C.UPDATE_ADD | 435 # we check if the entity is visible before changing anything |
436 # this way we know if we need to do an UPDATE_ADD, UPDATE_MODIFY | |
437 # or an UPDATE_DELETE | |
438 was_visible = self.entityVisible(entity_bare) | |
398 | 439 |
399 if in_roster: | 440 if in_roster: |
400 self._roster.add(entity_bare) | 441 self._roster.add(entity_bare) |
401 | 442 |
402 cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}, | 443 cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}, |
403 C.CONTACT_MAIN_RESOURCE: None, | 444 C.CONTACT_MAIN_RESOURCE: None, |
404 C.CONTACT_SELECTED: set()}) | 445 C.CONTACT_SELECTED: set()}) |
405 | 446 |
406 assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes | 447 # we don't want forbidden data in attributes |
448 assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) | |
407 | 449 |
408 # we set groups and fill self._groups accordingly | 450 # we set groups and fill self._groups accordingly |
409 if groups is not None: | 451 if groups is not None: |
410 if not groups: | 452 if not groups: |
411 groups = [None] # [None] is the default group | 453 groups = [None] # [None] is the default group |
412 if C.CONTACT_GROUPS in cache: | 454 if C.CONTACT_GROUPS in cache: |
413 # XXX: don't use set(cache[C.CONTACT_GROUPS]).difference(groups) because it won't work in Pyjamas if None is in cache[C.CONTACT_GROUPS] | 455 # XXX: don't use set(cache[C.CONTACT_GROUPS]).difference(groups) because |
414 for group in [group for group in cache[C.CONTACT_GROUPS] if group not in groups]: | 456 # it won't work in Pyjamas if None is in cache[C.CONTACT_GROUPS] |
457 for group in [group for group in cache[C.CONTACT_GROUPS] | |
458 if group not in groups]: | |
415 self._groups[group]['jids'].remove(entity_bare) | 459 self._groups[group]['jids'].remove(entity_bare) |
416 cache[C.CONTACT_GROUPS] = groups | 460 cache[C.CONTACT_GROUPS] = groups |
417 for group in groups: | 461 for group in groups: |
418 self._groups.setdefault(group, {}).setdefault('jids', set()).add(entity_bare) | 462 self._groups.setdefault(group, {}).setdefault('jids', set()).add( |
463 entity_bare) | |
419 | 464 |
420 # special entities management | 465 # special entities management |
421 if C.CONTACT_SPECIAL in attributes: | 466 if C.CONTACT_SPECIAL in attributes: |
422 if attributes[C.CONTACT_SPECIAL] is None: | 467 if attributes[C.CONTACT_SPECIAL] is None: |
423 del attributes[C.CONTACT_SPECIAL] | 468 del attributes[C.CONTACT_SPECIAL] |
426 self._specials.add(entity) | 471 self._specials.add(entity) |
427 cache[C.CONTACT_MAIN_RESOURCE] = None | 472 cache[C.CONTACT_MAIN_RESOURCE] = None |
428 | 473 |
429 # now the attributes we keep in cache | 474 # now the attributes we keep in cache |
430 # XXX: if entity is a full jid, we store the value for the resource only | 475 # XXX: if entity is a full jid, we store the value for the resource only |
431 cache_attr = cache[C.CONTACT_RESOURCES].setdefault(entity.resource, {}) if entity.resource else cache | 476 cache_attr = (cache[C.CONTACT_RESOURCES].setdefault(entity.resource, {}) |
477 if entity.resource else cache) | |
432 for attribute, value in attributes.iteritems(): | 478 for attribute, value in attributes.iteritems(): |
433 if value is None: | 479 if value is None: |
434 # XXX: pyjamas hack: we need to use pop instead of del | 480 # XXX: pyjamas hack: we need to use pop instead of del |
435 try: | 481 try: |
436 cache_attr[attribute].pop(value) | 482 cache_attr[attribute].pop(value) |
437 except KeyError: | 483 except KeyError: |
438 pass | 484 pass |
439 else: | 485 else: |
440 if attribute == 'nick' and self.isSpecial(entity, C.CONTACT_SPECIAL_GROUP): | 486 if attribute == 'nick' and self.isSpecial(entity, C.CONTACT_SPECIAL_GROUP): |
441 # we don't want to keep nick for MUC rooms | 487 # we don't want to keep nick for MUC rooms |
442 # FIXME: this is here as plugin XEP-0054 can link resource's nick with bare jid | 488 # FIXME: this is here as plugin XEP-0054 can link resource's nick |
443 # which in the case of MUC set the nick for the whole MUC | 489 # with bare jid which in the case of MUC |
490 # set the nick for the whole MUC | |
444 # resulting in bad name displayed in some frontends | 491 # resulting in bad name displayed in some frontends |
445 continue | 492 continue |
446 cache_attr[attribute] = value | 493 cache_attr[attribute] = value |
447 | 494 |
448 # we can update the display | 495 # we can update the display if needed |
449 self.update([entity], update_type, self.profile) | 496 if self.entityVisible(entity_bare): |
450 | 497 # if the contact was not visible, we need to add a widget |
451 def entityToShow(self, entity, check_resource=False): | 498 # else we just update id |
499 update_type = C.UPDATE_MODIFY if was_visible else C.UPDATE_ADD | |
500 self.update([entity], update_type, self.profile) | |
501 elif was_visible: | |
502 # the entity was visible and is not anymore, we remove it | |
503 self.update([entity], C.UPDATE_DELETE, self.profile) | |
504 | |
505 def entityVisible(self, entity, check_resource=False): | |
452 """Tell if the contact should be showed or hidden. | 506 """Tell if the contact should be showed or hidden. |
453 | 507 |
454 @param entity (jid.JID): jid of the contact | 508 @param entity (jid.JID): jid of the contact |
455 @param check_resource (bool): True if resource must be significant | 509 @param check_resource (bool): True if resource must be significant |
456 @return (bool): True if that contact should be showed in the list | 510 @return (bool): True if that contact should be showed in the list |
457 """ | 511 """ |
458 show = self.getCache(entity, C.PRESENCE_SHOW) | 512 try: |
513 show = self.getCache(entity, C.PRESENCE_SHOW) | |
514 except exceptions.NotFound: | |
515 return False | |
459 | 516 |
460 if check_resource: | 517 if check_resource: |
461 selected = self._selected | 518 selected = self._selected |
462 else: | 519 else: |
463 selected = {selected.bare for selected in self._selected} | 520 selected = {selected.bare for selected in self._selected} |
464 return ((show is not None and show != C.PRESENCE_UNAVAILABLE) | 521 return ((show is not None and show != C.PRESENCE_UNAVAILABLE) |
465 or self.show_disconnected | 522 or self.show_disconnected |
466 or entity in selected | 523 or entity in selected |
467 or next(self.host.getNotifs(entity.bare, profile=self.profile), None) | 524 or (self.show_entities_with_notifs |
525 and next(self.host.getNotifs(entity.bare, profile=self.profile), None)) | |
468 ) | 526 ) |
469 | 527 |
470 def anyEntityToShow(self, entities, check_resources=False): | 528 def anyEntityVisible(self, entities, check_resources=False): |
471 """Tell if in a list of entities, at least one should be shown | 529 """Tell if in a list of entities, at least one should be shown |
472 | 530 |
473 @param entities (list[jid.JID]): list of jids | 531 @param entities (list[jid.JID]): list of jids |
474 @param check_resources (bool): True if resources must be significant | 532 @param check_resources (bool): True if resources must be significant |
475 @return (bool): True if a least one entity need to be shown | 533 @return (bool): True if a least one entity need to be shown |
476 """ | 534 """ |
477 # FIXME: looks inefficient, really needed? | 535 # FIXME: looks inefficient, really needed? |
478 for entity in entities: | 536 for entity in entities: |
479 if self.entityToShow(entity, check_resources): | 537 if self.entityVisible(entity, check_resources): |
480 return True | 538 return True |
481 return False | 539 return False |
482 | 540 |
483 def isEntityInGroup(self, entity, group): | 541 def isEntityInGroup(self, entity, group): |
484 """Tell if an entity is in a roster group | 542 """Tell if an entity is in a roster group |
493 """remove a contact from the list | 551 """remove a contact from the list |
494 | 552 |
495 @param entity(jid.JID): jid of the entity to remove (bare jid is used) | 553 @param entity(jid.JID): jid of the entity to remove (bare jid is used) |
496 """ | 554 """ |
497 entity_bare = entity.bare | 555 entity_bare = entity.bare |
556 was_visible = self.entityVisible(entity_bare) | |
498 try: | 557 try: |
499 groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set()) | 558 groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set()) |
500 except KeyError: | 559 except KeyError: |
501 log.error(_(u"Trying to delete an unknow entity [{}]").format(entity)) | 560 log.error(_(u"Trying to delete an unknow entity [{}]").format(entity)) |
502 try: | 561 try: |
505 pass | 564 pass |
506 del self._cache[entity_bare] | 565 del self._cache[entity_bare] |
507 for group in groups: | 566 for group in groups: |
508 self._groups[group]['jids'].remove(entity_bare) | 567 self._groups[group]['jids'].remove(entity_bare) |
509 if not self._groups[group]['jids']: | 568 if not self._groups[group]['jids']: |
510 self._groups.pop(group) # FIXME: we use pop because of pyjamas: http://wiki.goffi.org/wiki/Issues_with_Pyjamas/en | 569 # FIXME: we use pop because of pyjamas: |
570 # http://wiki.goffi.org/wiki/Issues_with_Pyjamas/en | |
571 self._groups.pop(group) | |
511 for iterable in (self._selected, self._specials): | 572 for iterable in (self._selected, self._specials): |
512 to_remove = set() | 573 to_remove = set() |
513 for set_entity in iterable: | 574 for set_entity in iterable: |
514 if set_entity.bare == entity.bare: | 575 if set_entity.bare == entity.bare: |
515 to_remove.add(set_entity) | 576 to_remove.add(set_entity) |
516 iterable.difference_update(to_remove) | 577 iterable.difference_update(to_remove) |
517 self.update([entity], C.UPDATE_DELETE, self.profile) | 578 if was_visible: |
579 self.update([entity], C.UPDATE_DELETE, self.profile) | |
518 | 580 |
519 def onPresenceUpdate(self, entity, show, priority, statuses, profile): | 581 def onPresenceUpdate(self, entity, show, priority, statuses, profile): |
520 """Update entity's presence status | 582 """Update entity's presence status |
521 | 583 |
522 @param entity(jid.JID): entity updated | 584 @param entity(jid.JID): entity updated |
523 @param show: availability | 585 @param show: availability |
524 @parap priority: resource's priority | 586 @parap priority: resource's priority |
525 @param statuses: dict of statuses | 587 @param statuses: dict of statuses |
526 @param profile: %(doc_profile)s | 588 @param profile: %(doc_profile)s |
527 """ | 589 """ |
528 cache = self.getCache(entity) | 590 # FIXME: cache modification should be done with setContact |
591 # the resources/presence handling logic should be moved there | |
592 was_visible = self.entityVisible(entity.bare) | |
593 cache = self.getCache(entity, create_if_not_found=True) | |
529 if show == C.PRESENCE_UNAVAILABLE: | 594 if show == C.PRESENCE_UNAVAILABLE: |
530 if not entity.resource: | 595 if not entity.resource: |
531 cache[C.CONTACT_RESOURCES].clear() | 596 cache[C.CONTACT_RESOURCES].clear() |
532 cache[C.CONTACT_MAIN_RESOURCE] = None | 597 cache[C.CONTACT_MAIN_RESOURCE] = None |
533 else: | 598 else: |
534 try: | 599 try: |
535 del cache[C.CONTACT_RESOURCES][entity.resource] | 600 del cache[C.CONTACT_RESOURCES][entity.resource] |
536 except KeyError: | 601 except KeyError: |
537 log.error(u"Presence unavailable received for an unknown resource [{}]".format(entity)) | 602 log.error(u"Presence unavailable received " |
603 u"for an unknown resource [{}]".format(entity)) | |
538 if not cache[C.CONTACT_RESOURCES]: | 604 if not cache[C.CONTACT_RESOURCES]: |
539 cache[C.CONTACT_MAIN_RESOURCE] = None | 605 cache[C.CONTACT_MAIN_RESOURCE] = None |
540 else: | 606 else: |
541 if not entity.resource: | 607 if not entity.resource: |
542 log.warning(_(u"received presence from entity without resource: {}".format(entity))) | 608 log.warning(_(u"received presence from entity " |
609 u"without resource: {}".format(entity))) | |
543 resources_data = cache[C.CONTACT_RESOURCES] | 610 resources_data = cache[C.CONTACT_RESOURCES] |
544 resource_data = resources_data.setdefault(entity.resource, {}) | 611 resource_data = resources_data.setdefault(entity.resource, {}) |
545 resource_data[C.PRESENCE_SHOW] = show | 612 resource_data[C.PRESENCE_SHOW] = show |
546 resource_data[C.PRESENCE_PRIORITY] = int(priority) | 613 resource_data[C.PRESENCE_PRIORITY] = int(priority) |
547 resource_data[C.PRESENCE_STATUSES] = statuses | 614 resource_data[C.PRESENCE_STATUSES] = statuses |
548 | 615 |
549 if entity.bare not in self._specials: | 616 if entity.bare not in self._specials: |
550 # we may have resources with no priority | 617 # we may have resources with no priority |
551 # (when a cached value is added for a not connected resource) | 618 # (when a cached value is added for a not connected resource) |
552 priority_resource = max(resources_data, key=lambda res: resources_data[res].get(C.PRESENCE_PRIORITY, -2**32)) | 619 priority_resource = max(resources_data, |
620 key=lambda res: resources_data[res].get( | |
621 C.PRESENCE_PRIORITY, -2**32)) | |
553 cache[C.CONTACT_MAIN_RESOURCE] = priority_resource | 622 cache[C.CONTACT_MAIN_RESOURCE] = priority_resource |
554 self.update([entity], C.UPDATE_MODIFY, self.profile) | 623 if self.entityVisible(entity.bare): |
624 update_type = C.UPDATE_MODIFY if was_visible else C.UPDATE_ADD | |
625 self.update([entity], update_type, self.profile) | |
626 elif was_visible: | |
627 self.update([entity], C.UPDATE_DELETE, self.profile) | |
555 | 628 |
556 def onNickUpdate(self, entity, new_nick, profile): | 629 def onNickUpdate(self, entity, new_nick, profile): |
557 """Update entity's nick | 630 """Update entity's nick |
558 | 631 |
559 @param entity(jid.JID): entity updated | 632 @param entity(jid.JID): entity updated |
560 @param new_nick(unicode): new nick of the entity | 633 @param new_nick(unicode): new nick of the entity |
561 @param profile: %(doc_profile)s | 634 @param profile: %(doc_profile)s |
562 """ | 635 """ |
563 assert profile == self.profile | 636 assert profile == self.profile |
564 self.setCache(entity, 'nick', new_nick) | 637 self.setCache(entity, 'nick', new_nick) |
565 self.update([entity], C.UPDATE_MODIFY, profile) | |
566 | 638 |
567 def onNotification(self, entity, notif, profile): | 639 def onNotification(self, entity, notif, profile): |
568 """Update entity with notification | 640 """Update entity with notification |
569 | 641 |
570 @param entity(jid.JID): entity updated | 642 @param entity(jid.JID): entity updated |
571 @param notif(dict): notification data | 643 @param notif(dict): notification data |
572 @param profile: %(doc_profile)s | 644 @param profile: %(doc_profile)s |
573 """ | 645 """ |
574 assert profile == self.profile | 646 assert profile == self.profile |
575 if entity is not None: | 647 if entity is not None and self.entityVisible(entity): |
576 self.update([entity], C.UPDATE_MODIFY, profile) | 648 self.update([entity], C.UPDATE_MODIFY, profile) |
577 | 649 |
578 def unselect(self, entity): | 650 def unselect(self, entity): |
579 """Unselect an entity | 651 """Unselect an entity |
580 | 652 |
614 cache[C.CONTACT_SELECTED].add(entity.resource) | 686 cache[C.CONTACT_SELECTED].add(entity.resource) |
615 self._selected.add(entity) | 687 self._selected.add(entity) |
616 self.update([entity], C.UPDATE_SELECTION, profile=self.profile) | 688 self.update([entity], C.UPDATE_SELECTION, profile=self.profile) |
617 | 689 |
618 def showOfflineContacts(self, show): | 690 def showOfflineContacts(self, show): |
619 """Tell if offline contacts should shown | 691 """Tell if offline contacts should be shown |
620 | 692 |
621 @param show(bool): True if offline contacts should be shown | 693 @param show(bool): True if offline contacts should be shown |
622 """ | 694 """ |
623 assert isinstance(show, bool) | 695 assert isinstance(show, bool) |
624 if self.show_disconnected == show: | 696 if self.show_disconnected == show: |
636 def showResources(self, show): | 708 def showResources(self, show): |
637 assert isinstance(show, bool) | 709 assert isinstance(show, bool) |
638 if self.show_resources == show: | 710 if self.show_resources == show: |
639 return | 711 return |
640 self.show_resources = show | 712 self.show_resources = show |
641 self.update(profile=self.profile) | 713 self.update(type_=C.UPDATE_STRUCTURE, profile=self.profile) |
642 | 714 |
643 def plug(self): | 715 def plug(self): |
644 handler.addProfile(self.profile) | 716 handler.addProfile(self.profile) |
645 | 717 |
646 def unplug(self): | 718 def unplug(self): |
655 def __init__(self, host): | 727 def __init__(self, host): |
656 super(QuickContactListHandler, self).__init__() | 728 super(QuickContactListHandler, self).__init__() |
657 self.host = host | 729 self.host = host |
658 global handler | 730 global handler |
659 if handler is not None: | 731 if handler is not None: |
660 raise exceptions.InternalError(u"QuickContactListHandler must be instanciated only once") | 732 raise exceptions.InternalError(u"QuickContactListHandler must be " |
733 u"instanciated only once") | |
661 handler = self | 734 handler = self |
662 self._clist = {} # key: profile, value: ProfileContactList | 735 self._clist = {} # key: profile, value: ProfileContactList |
663 self._widgets = set() | 736 self._widgets = set() |
664 self._update_locked = False # se to True to ignore updates | 737 self._update_locked = False # se to True to ignore updates |
665 | 738 |
668 return self._clist[profile] | 741 return self._clist[profile] |
669 | 742 |
670 def __contains__(self, entity): | 743 def __contains__(self, entity): |
671 """Check if entity is in contact list | 744 """Check if entity is in contact list |
672 | 745 |
673 @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed) | 746 @param entity (jid.JID): jid of the entity (resource is not ignored, |
747 use bare jid if needed) | |
674 """ | 748 """ |
675 for contact_list in self._clist.itervalues(): | 749 for contact_list in self._clist.itervalues(): |
676 if entity in contact_list: | 750 if entity in contact_list: |
677 return True | 751 return True |
678 return False | 752 return False |
823 def getSpecialExtras(self, special_type=None): | 897 def getSpecialExtras(self, special_type=None): |
824 """Return special extras with given type | 898 """Return special extras with given type |
825 | 899 |
826 If special_type is None, return all special extras. | 900 If special_type is None, return all special extras. |
827 | 901 |
828 @param special_type(unicode, None): one of special type (e.g. C.CONTACT_SPECIAL_GROUP) | 902 @param special_type(unicode, None): one of special type |
903 (e.g. C.CONTACT_SPECIAL_GROUP) | |
829 None to return all special extras. | 904 None to return all special extras. |
830 @return (set[jid.JID]) | 905 @return (set[jid.JID]) |
831 """ | 906 """ |
832 entities = set() | 907 entities = set() |
833 for contact_list in self._clist.itervalues(): | 908 for contact_list in self._clist.itervalues(): |
836 | 911 |
837 def _contactsFilled(self, profile): | 912 def _contactsFilled(self, profile): |
838 self._to_fill.remove(profile) | 913 self._to_fill.remove(profile) |
839 if not self._to_fill: | 914 if not self._to_fill: |
840 del self._to_fill | 915 del self._to_fill |
916 # we need a full update when all contacts are filled | |
841 self.update() | 917 self.update() |
842 | 918 |
843 def fill(self, profile=None): | 919 def fill(self, profile=None): |
844 """Get all contacts from backend, and fill the widget | 920 """Get all contacts from backend, and fill the widget |
845 | 921 |
862 else: | 938 else: |
863 to_fill.update(self._clist.items()) | 939 to_fill.update(self._clist.items()) |
864 | 940 |
865 remaining = to_fill.difference(filled) | 941 remaining = to_fill.difference(filled) |
866 if remaining != to_fill: | 942 if remaining != to_fill: |
867 log.debug(u"Not re-filling already filled contact list(s) for {}".format(u', '.join(to_fill.intersection(filled)))) | 943 log.debug(u"Not re-filling already filled contact list(s) for {}".format( |
944 u', '.join(to_fill.intersection(filled)))) | |
868 for profile in remaining: | 945 for profile in remaining: |
869 self._clist[profile]._fill() | 946 self._clist[profile]._fill() |
870 | 947 |
871 def clearContacts(self, keep_cache=False): | 948 def clearContacts(self, keep_cache=False): |
872 """Clear all the contact list | 949 """Clear all the contact list |
873 | 950 |
874 @param keep_cache: if True, don't reset the cache | 951 @param keep_cache: if True, don't reset the cache |
875 """ | 952 """ |
876 for contact_list in self._clist.itervalues(): | 953 for contact_list in self._clist.itervalues(): |
877 contact_list.clearContacts(keep_cache) | 954 contact_list.clearContacts(keep_cache) |
955 # we need a full update | |
878 self.update() | 956 self.update() |
879 | 957 |
880 def select(self, entity): | 958 def select(self, entity): |
881 for contact_list in self._clist.itervalues(): | 959 for contact_list in self._clist.itervalues(): |
882 contact_list.select(entity) | 960 contact_list.select(entity) |
893 @param locked(bool): updates are forbidden if True | 971 @param locked(bool): updates are forbidden if True |
894 @param do_update(bool): if True, a full update is done after unlocking | 972 @param do_update(bool): if True, a full update is done after unlocking |
895 if set to False, widget state can be inconsistent, be sure to know | 973 if set to False, widget state can be inconsistent, be sure to know |
896 what youa re doing! | 974 what youa re doing! |
897 """ | 975 """ |
898 log.debug(u"Contact lists updates are now {}".format(u"LOCKED" if locked else u"UNLOCKED")) | 976 log.debug(u"Contact lists updates are now {}".format( |
977 u"LOCKED" if locked else u"UNLOCKED")) | |
899 self._update_locked = locked | 978 self._update_locked = locked |
900 if not locked and do_update: | 979 if not locked and do_update: |
901 self.update() | 980 self.update() |
902 | 981 |
903 def update(self, entities=None, type_=None, profile=None): | 982 def update(self, entities=None, type_=None, profile=None): |
908 | 987 |
909 class QuickContactList(QuickWidget): | 988 class QuickContactList(QuickWidget): |
910 """This class manage the visual representation of contacts""" | 989 """This class manage the visual representation of contacts""" |
911 SINGLE=False | 990 SINGLE=False |
912 PROFILES_MULTIPLE=True | 991 PROFILES_MULTIPLE=True |
913 PROFILES_ALLOW_NONE=True # Can be linked to no profile (e.g. at the early forntend start) | 992 # Can be linked to no profile (e.g. at the early frontend start) |
993 PROFILES_ALLOW_NONE=True | |
914 | 994 |
915 def __init__(self, host, profiles): | 995 def __init__(self, host, profiles): |
916 super(QuickContactList, self).__init__(host, None, profiles) | 996 super(QuickContactList, self).__init__(host, None, profiles) |
917 | 997 |
918 # options | 998 # options |
935 def items(self): | 1015 def items(self): |
936 return handler.items | 1016 return handler.items |
937 | 1017 |
938 @property | 1018 @property |
939 def items_sorted(self): | 1019 def items_sorted(self): |
940 return handler.items | 1020 return handler.items_sorted |
941 | 1021 |
942 def update(self, entities=None, type_=None, profile=None): | 1022 def update(self, entities=None, type_=None, profile=None): |
943 """Update the display when something changed | 1023 """Update the display when something changed |
944 | 1024 |
945 @param entities(iterable[jid.JID], None): updated entities, | 1025 @param entities(iterable[jid.JID], None): updated entities, |
948 - C.UPDATE_DELETE: entity deleted | 1028 - C.UPDATE_DELETE: entity deleted |
949 - C.UPDATE_MODIFY: entity updated | 1029 - C.UPDATE_MODIFY: entity updated |
950 - C.UPDATE_ADD: entity added | 1030 - C.UPDATE_ADD: entity added |
951 - C.UPDATE_SELECTION: selection modified | 1031 - C.UPDATE_SELECTION: selection modified |
952 or None for undefined update | 1032 or None for undefined update |
1033 Note that events correspond to addition, modification and deletion | |
1034 of items on the whole contact list. If the contact is visible or not | |
1035 has no influence on the type_. | |
953 @param profile(unicode, None): profile concerned with the update | 1036 @param profile(unicode, None): profile concerned with the update |
954 None if unknown | 1037 None if all profiles need to be updated |
955 """ | 1038 """ |
956 raise NotImplementedError | 1039 raise NotImplementedError |
957 | 1040 |
958 def onDelete(self): | 1041 def onDelete(self): |
959 QuickWidget.onDelete(self) | 1042 QuickWidget.onDelete(self) |