Skip to content
This repository has been archived by the owner on Mar 22, 2018. It is now read-only.

Commit

Permalink
I would dare say this is ready. but the MVP for species is NOT unit t…
Browse files Browse the repository at this point in the history
…estable.

so I am not so sure how to "sell" this to my users...
#221
  • Loading branch information
mfrasca committed Dec 23, 2015
1 parent 1d9e233 commit a176235
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 30 deletions.
47 changes: 29 additions & 18 deletions bauble/plugins/plants/ask_tpl.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
class AskTPL(threading.Thread):
running = None

def __init__(self, binomial, callback, threshold=0.8, timeout=4,
def __init__(self, binomial, callback, threshold=0.8, timeout=4, gui=False,
group=None, verbose=None, **kwargs):
super(AskTPL, self).__init__(
group=group, target=None, name=None, verbose=verbose)
Expand All @@ -53,6 +53,7 @@ def __init__(self, binomial, callback, threshold=0.8, timeout=4,
self.threshold = threshold
self.callback = callback
self.timeout = timeout
self.gui = gui

def stop(self):
self._stop = True
Expand All @@ -71,7 +72,7 @@ def ask_tpl(binomial):
for k in l if k)]
header = result[0]
result = result[1:]
return [dict(zip(header, k)) for k in result]
return [dict(zip(header, k)) for k in result if k[7] == '']

class ShouldStopNow(Exception):
pass
Expand All @@ -83,41 +84,48 @@ class NoResult(Exception):
return

try:
synonym = None
accepted = None
logger.debug("%s before first query", self.name)
candidates = ask_tpl(self.binomial)
logger.debug("%s after first query", self.name)
if self.stopped():
raise ShouldStopNow('after first query')
if len(candidates) > 1:
l = []
for candidate in candidates:
g, s = candidate['Genus'], candidate['Species']
for item in candidates:
g, s = item['Genus'], item['Species']
seq = difflib.SequenceMatcher(a=self.binomial,
b='%s %s' % (g, s))
l.append((seq.ratio(), candidate))
l.append((seq.ratio(), item))

score, candidate = sorted(l)[-1]
score, found = sorted(l)[-1]
if score < self.threshold:
score = 0
elif candidates:
candidate = candidates.pop()
found = candidates.pop()
else:
raise NoResult
if candidate['Accepted ID']:
synonym = candidate
candidate = ask_tpl(candidate['Accepted ID'])[0]
if found['Accepted ID']:
accepted = ask_tpl(found['Accepted ID'])[0]
logger.debug("%s after second query", self.name)
if self.stopped():
raise ShouldStopNow('after second query')
except ShouldStopNow:
logger.debug("%s interrupted : do not invoke callback",
self.name)
return
except Exception, e:
logger.debug("%s (%s)%s : do not invoke callback",
logger.debug("%s (%s)%s : completed with trouble",
self.name, type(e).__name__, e)
self.__class__.running = None
return
found = accepted = None
self.__class__.running = None
logger.debug("%s before invoking callback" % self.name)
self.callback(candidate, synonym)
if self.gui:
import gobject
gobject.idle_add(self.callback, found, accepted)
else:
self.callback(found, accepted)


def citation(d):
Expand All @@ -126,10 +134,13 @@ def citation(d):
"%(Authorship)s (%(Family)s)" % d


def what_to_do_with_it(accepted, synonym):
if synonym is not None:
logger.debug("%s - synonym of:", citation(synonym))
logger.debug("%s", citation(accepted))
def what_to_do_with_it(found, accepted):
if found is None and accepted is None:
logger.info("nothing matches")
return
logger.info("%s", citation(found))
if accepted is not None:
logger.info("%s - is its accepted form", citation(accepted))


if __name__ == '__main__':
Expand Down
3 changes: 2 additions & 1 deletion bauble/plugins/plants/species_editor.glade
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<signal name="changed" handler="on_entry_changed_clear_boxes" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
Expand Down Expand Up @@ -299,7 +300,7 @@
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<signal name="changed" handler="on_text_entry_changed" swapped="no"/>
<signal name="changed" handler="on_sp_species_entry_changed" swapped="no"/>
<signal name="insert-text" handler="on_sp_species_entry_insert_text" swapped="no"/>
</object>
<packing>
Expand Down
131 changes: 120 additions & 11 deletions bauble/plugins/plants/species_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def __init__(self, model, view):
self.session = object_session(model)
self._dirty = False
self.omonym_box = None
self.species_check_messages = []
self.genus_check_messages = []
self.species_space = False # do not accept spaces in epithet
self.init_fullname_widgets()
self.vern_presenter = VernacularNamePresenter(self)
Expand Down Expand Up @@ -123,31 +125,125 @@ def gen_get_completions(text):
return self.session.query(Genus).filter(clause).\
order_by(Genus.genus)

def sp_species_TPL_callback(found, accepted):
# both found and accepted are dictionaries, their keys here
# relevant: 'Species hybrid marker', 'Species', 'Authorship',
# 'Taxonomic status in TPL'.

# we can provide the user the option to accept spellings
# corrections in 'Species', the full value of 'Authorship', and
# full acceptedy links. it's TWO boxes that we might show. or
# one if nothing matches.

if found:
found = dict((k, utils.to_unicode(v))
for k, v in found.items())
if accepted:
accepted = dict((k, utils.to_unicode(v))
for k, v in accepted.items())

msg_box_msg = _('No match found on ThePlantList.org')

if not (found is None and accepted is None):

# if inserted data matches found, just say so.
if (self.model.sp == found['Species'] and
self.model.sp_author == found['Authorship'] and
self.model.hybrid == (
found['Species hybrid marker'] == u'×')):
msg_box_msg = _(
'your data finely matches ThePlantList.org')
else:
cit = ('<i>%(Genus)s</i> %(Species hybrid marker)s'
'<i>%(Species)s</i> %(Authorship)s (%(Family)s)'
) % found
msg = _('%s is the closest match for your data.\n'
'Do you want to accept it?' % cit)
b1 = box = self.view.add_message_box(
utils.MESSAGE_BOX_YESNO)
box.message = msg

def on_response_found(button, response):
self.view.remove_box(b1)
if response:
self.set_model_attr('sp', found['Species'])
self.set_model_attr('sp_author', found['Authorship'])
self.set_model_attr(
'hybrid',
found['Species hybrid marker'] == u'×')
self.refresh_view()
self.refresh_fullname_label()
box.on_response = on_response_found
box.show()
self.view.add_box(box)
self.species_check_messages.append(box)
msg_box_msg = None

if self.model.accepted is None and accepted:
cit = ('<i>%(Genus)s</i> %(Species hybrid marker)s'
'<i>%(Species)s</i> %(Authorship)s (%(Family)s)'
) % accepted
msg = _('%s is the accepted taxon for your data.\n'
'Do you want to add it?' % cit)
b2 = box = self.view.add_message_box(
utils.MESSAGE_BOX_YESNO)
box.message = msg

def on_response_accepted(button, response):
self.view.remove_box(b2)
if response:
hybrid = accepted['Species hybrid marker'] == u'×'
self.model.accepted = Species.retrieve_or_create(
self.session, {
'object': 'taxon',
'rank': 'species',
'ht-rank': 'genus',
'ht-epithet': accepted['Genus'],
'epithet': accepted['Species'],
'sp_author': accepted['Authorship'],
'hybrid': hybrid}
)
self.refresh_view()
self.refresh_fullname_label()
box.on_response = on_response_accepted
box.show()
self.view.add_box(box)
self.species_check_messages.append(box)
msg_box_msg = None

if msg_box_msg is not None:
b0 = self.view.add_message_box(utils.MESSAGE_BOX_INFO)
b0.message = msg_box_msg
b0.on_response = lambda b, r: self.view.remove_box(b0)
b0.show()
self.view.add_box(b0)
self.species_check_messages.append(b0)

def on_sp_species_button_clicked(widget, event=None):
# this activity runs in a separate thread.
# return False
# Any code that modifies the UI that is called from outside the
# main thread must be pushed into the main thread and called
# asynchronously in the main loop, with gobject.idle_add.
from ask_tpl import AskTPL, what_to_do_with_it
# the real activity runs in a separate thread.
from ask_tpl import AskTPL

while self.species_check_messages:
kid = self.species_check_messages.pop()
self.view.widgets.remove_parent(kid)

binomial = '%s %s' % (self.model.genus, self.model.sp)
AskTPL(binomial, what_to_do_with_it, timeout=2).start()
AskTPL(binomial, sp_species_TPL_callback, timeout=2, gui=True
).start()
if event is not None:
return False

self.view.connect('sp_species_button', "clicked",
on_sp_species_button_clicked)
self.view.connect('sp_species_entry', "focus-out-event",
on_sp_species_button_clicked)

# called when a genus is selected from the genus completions
def on_select(value):
logger.debug('on select: %s' % value)
if isinstance(value, StringTypes):
value = self.session.query(Genus).filter(
Genus.genus == value).first()
for kid in self.view.widgets.message_box_parent.get_children():
while self.genus_check_messages:
kid = self.genus_check_messages.pop()
self.view.widgets.remove_parent(kid)
self.set_model_attr('genus', value)
if not value: # no choice is a fine choice
Expand Down Expand Up @@ -179,6 +275,7 @@ def on_response(button, response):
box.on_response = on_response
box.show()
self.view.add_box(box)
self.genus_check_messages.append(box)

on_select(self.model.genus)

Expand All @@ -202,6 +299,15 @@ def on_response(button, response):
except Exception:
pass

def on_sp_species_entry_changed(self, widget, *args):
self.on_text_entry_changed(widget, *args)
self.on_entry_changed_clear_boxes(widget, *args)

def on_entry_changed_clear_boxes(self, widget, *args):
while self.species_check_messages:
kid = self.species_check_messages.pop()
self.view.widgets.remove_parent(kid)

def on_habit_comboentry_changed(self, combo, *args):
"""
Changed handler for sp_habit_comboentry.
Expand Down Expand Up @@ -281,6 +387,10 @@ def on_sp_species_entry_insert_text(self, entry, text, length, position):
'''remove all spaces from epithet
'''

while self.species_check_messages:
kid = self.species_check_messages.pop()
self.view.widgets.remove_parent(kid)

# get position from entry, can't trust position parameter
position = entry.get_position()
if text.count(u'×'):
Expand Down Expand Up @@ -986,7 +1096,6 @@ def __init__(self, parent=None):
w.set_geometry_hints(
max_width=gtk.gdk.screen_get_default().get_width())
w.set_position(gtk.WIN_POS_NONE)
print gtk.gdk.screen_get_default().get_width(), gtk.WIN_POS_CENTER_ON_PARENT

def get_window(self):
'''
Expand Down

0 comments on commit a176235

Please sign in to comment.