mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 08:46:37 -04:00
improved finding
- new per-character tokenizer that is easier to read and maintain - OR searches now supported - grouping with ( and ) now supported - sorting is off by default now - searching in html is disabled for now, but may come back later - use ? arguments instead of named arguments to ease porting to other platforms - field:foo is no longer wrapped in implicit wildcards - avoid joining notes table if it not required - about 30% faster - fixed sql injection bug in findNids() - commands no longer have to take care of negation themselves - commands can return nothing to immediately flag the query as invalid
This commit is contained in:
parent
ef4ff86f8c
commit
6af7f6e846
4 changed files with 297 additions and 355 deletions
|
@ -510,8 +510,8 @@ where c.nid == f.id
|
|||
# Finding cards
|
||||
##########################################################################
|
||||
|
||||
def findCards(self, query, full=False, order=None):
|
||||
return anki.find.Finder(self).findCards(query, full, order)
|
||||
def findCards(self, query, order=False):
|
||||
return anki.find.Finder(self).findCards(query, order)
|
||||
|
||||
def findReplace(self, nids, src, dst, regex=None, field=None, fold=True):
|
||||
return anki.find.findReplace(self, nids, src, dst, regex, field, fold)
|
||||
|
|
572
anki/find.py
572
anki/find.py
|
@ -6,32 +6,6 @@ import re
|
|||
from anki.utils import ids2str, splitFields, joinFields, stripHTML, intTime
|
||||
from anki.consts import *
|
||||
|
||||
SEARCH_TAG = 0
|
||||
SEARCH_TYPE = 1
|
||||
SEARCH_PHRASE = 2
|
||||
SEARCH_NID = 3
|
||||
SEARCH_TEMPLATE = 4
|
||||
SEARCH_FIELD = 5
|
||||
SEARCH_MODEL = 6
|
||||
SEARCH_DECK = 7
|
||||
SEARCH_PROP = 8
|
||||
SEARCH_RATED = 9
|
||||
|
||||
# Tools
|
||||
##########################################################################
|
||||
|
||||
def fieldNames(col, downcase=True):
|
||||
fields = set()
|
||||
names = []
|
||||
for m in col.models.all():
|
||||
for f in m['flds']:
|
||||
if f['name'].lower() not in fields:
|
||||
names.append(f['name'])
|
||||
fields.add(f['name'].lower())
|
||||
if downcase:
|
||||
return list(fields)
|
||||
return names
|
||||
|
||||
# Find
|
||||
##########################################################################
|
||||
|
||||
|
@ -40,37 +14,172 @@ class Finder(object):
|
|||
def __init__(self, col):
|
||||
self.col = col
|
||||
|
||||
def findCards(self, query, full=False, order=None):
|
||||
def findCards(self, query, order=False):
|
||||
"Return a list of card ids for QUERY."
|
||||
self.order = order
|
||||
self.query = query
|
||||
self.full = full
|
||||
self._findLimits()
|
||||
if not self.lims['valid']:
|
||||
tokens = self._tokenize(query)
|
||||
preds, args = self._where(tokens)
|
||||
if preds is None:
|
||||
return []
|
||||
(q, args) = self._whereClause()
|
||||
order = self._order()
|
||||
query = """\
|
||||
select c.id from cards c, notes n where %s
|
||||
and c.nid=n.id %s""" % (q, order)
|
||||
res = self.col.db.list(query, **args)
|
||||
if not self.order and self.col.conf['sortBackwards']:
|
||||
order, rev = self._order(order)
|
||||
sql = self._query(preds, order)
|
||||
res = self.col.db.list(sql, *args)
|
||||
if rev:
|
||||
res.reverse()
|
||||
return res
|
||||
|
||||
def _whereClause(self):
|
||||
q = " and ".join(self.lims['preds'])
|
||||
if not q:
|
||||
q = "1"
|
||||
return q, self.lims['args']
|
||||
# Tokenizing
|
||||
######################################################################
|
||||
|
||||
def _order(self):
|
||||
# user provided override?
|
||||
if self.order:
|
||||
return self.order
|
||||
def _tokenize(self, query):
|
||||
inQuote = False
|
||||
tokens = []
|
||||
token = ""
|
||||
for c in query:
|
||||
# quoted text
|
||||
if c in ("'", '"'):
|
||||
if inQuote:
|
||||
if c == inQuote:
|
||||
inQuote = False
|
||||
else:
|
||||
token += c
|
||||
elif token:
|
||||
# quotes are allowed to start directly after a :
|
||||
if token[-1] == ":":
|
||||
inQuote = c
|
||||
else:
|
||||
token += c
|
||||
else:
|
||||
inQuote = c
|
||||
# separator
|
||||
elif c == " ":
|
||||
if inQuote:
|
||||
token += c
|
||||
elif token:
|
||||
# space marks token finished
|
||||
tokens.append(token)
|
||||
token = ""
|
||||
# nesting
|
||||
elif c in ("(", ")"):
|
||||
if inQuote:
|
||||
token += c
|
||||
else:
|
||||
if c == ")" and token:
|
||||
tokens.append(token)
|
||||
token = ""
|
||||
tokens.append(c)
|
||||
# negation
|
||||
elif c == "-":
|
||||
if token:
|
||||
token += c
|
||||
elif not tokens or tokens[-1] != "not":
|
||||
tokens.append("not")
|
||||
# normal character
|
||||
else:
|
||||
token += c
|
||||
# if we finished in a token, add it
|
||||
if token:
|
||||
tokens.append(token)
|
||||
return tokens
|
||||
|
||||
# Query building
|
||||
######################################################################
|
||||
|
||||
def _where(self, tokens):
|
||||
# state and query
|
||||
s = dict(isnot=False, isor=False, join=False, q="", bad=False)
|
||||
args = []
|
||||
def add(txt, wrap=True):
|
||||
# failed command?
|
||||
if not txt:
|
||||
# if it was to be negated then we can just ignore it
|
||||
if s['isnot']:
|
||||
s['isnot'] = False
|
||||
return
|
||||
else:
|
||||
s['bad'] = True
|
||||
return
|
||||
# do we need a conjunction?
|
||||
if s['join']:
|
||||
if s['isor']:
|
||||
s['q'] += " or "
|
||||
s['isor'] = False
|
||||
else:
|
||||
s['q'] += " and "
|
||||
if s['isnot']:
|
||||
s['q'] += " not "
|
||||
if wrap:
|
||||
txt = "(" + txt + ")"
|
||||
s['q'] += txt
|
||||
s['join'] = True
|
||||
for token in tokens:
|
||||
if s['bad']:
|
||||
return None, None
|
||||
# special tokens
|
||||
if token == "not":
|
||||
s['isnot'] = True
|
||||
elif token.lower() == "or":
|
||||
s['isor'] = True
|
||||
elif token == "(":
|
||||
add(token, wrap=False)
|
||||
s['join'] = False
|
||||
elif token == ")":
|
||||
s['q'] += ")"
|
||||
# commands
|
||||
elif ":" in token:
|
||||
cmd, val = token.split(":", 1)
|
||||
cmd = cmd.lower()
|
||||
if cmd == "tag":
|
||||
add(self._findTag(val, args))
|
||||
elif cmd == "is":
|
||||
add(self._findCardState(val))
|
||||
elif cmd == "nid":
|
||||
add(self._findNids(val))
|
||||
elif cmd == "card":
|
||||
add(self._findTemplate(val))
|
||||
elif cmd == "note":
|
||||
add(self._findModel(val))
|
||||
elif cmd == "deck":
|
||||
add(self._findDeck(val))
|
||||
elif cmd == "prop":
|
||||
add(self._findProp(val))
|
||||
elif cmd == "rated":
|
||||
add(self._findRated(val))
|
||||
else:
|
||||
add(self._findField(cmd, val))
|
||||
# normal text search
|
||||
else:
|
||||
add(self._findText(token, args))
|
||||
if s['bad']:
|
||||
return None, None
|
||||
return s['q'], args
|
||||
|
||||
def _query(self, preds, order):
|
||||
# can we skip the note table?
|
||||
if "n." not in preds and "n." not in order:
|
||||
sql = "select c.id from cards c where "
|
||||
else:
|
||||
sql = "select c.id from cards c, notes n where c.nid=n.id and "
|
||||
# combine with preds
|
||||
if preds:
|
||||
sql += "(" + preds + ")"
|
||||
else:
|
||||
sql += "1"
|
||||
# order
|
||||
if order:
|
||||
sql += " " + order
|
||||
return sql
|
||||
|
||||
# Ordering
|
||||
######################################################################
|
||||
|
||||
def _order(self, order):
|
||||
if not order:
|
||||
return "", False
|
||||
elif order is not True:
|
||||
# custom order string provided
|
||||
return " order by " + order, False
|
||||
# use deck default
|
||||
type = self.col.conf['sortType']
|
||||
if not type:
|
||||
return
|
||||
if type.startswith("note"):
|
||||
if type == "noteCrt":
|
||||
sort = "n.id, c.ord"
|
||||
|
@ -97,57 +206,23 @@ and c.nid=n.id %s""" % (q, order)
|
|||
raise Exception()
|
||||
else:
|
||||
raise Exception()
|
||||
return " order by " + sort
|
||||
return " order by " + sort, self.col.conf['sortBackwards']
|
||||
|
||||
def _findLimits(self):
|
||||
"Generate a list of note/card limits for the query."
|
||||
self.lims = {
|
||||
'preds': [],
|
||||
'args': {},
|
||||
'valid': True,
|
||||
}
|
||||
for c, (token, isNeg, type) in enumerate(self._parseQuery()):
|
||||
if type == SEARCH_TAG:
|
||||
self._findTag(token, isNeg, c)
|
||||
elif type == SEARCH_TYPE:
|
||||
self._findCardState(token, isNeg)
|
||||
elif type == SEARCH_NID:
|
||||
self._findNids(token)
|
||||
elif type == SEARCH_TEMPLATE:
|
||||
self._findTemplate(token, isNeg)
|
||||
elif type == SEARCH_FIELD:
|
||||
self._findField(token, isNeg)
|
||||
elif type == SEARCH_MODEL:
|
||||
self._findModel(token, isNeg)
|
||||
elif type == SEARCH_DECK:
|
||||
self._findDeck(token, isNeg)
|
||||
elif type == SEARCH_PROP:
|
||||
self._findProp(token, isNeg)
|
||||
elif type == SEARCH_RATED:
|
||||
self._findRated(token, isNeg)
|
||||
else:
|
||||
self._findText(token, isNeg, c)
|
||||
# Commands
|
||||
######################################################################
|
||||
|
||||
def _findTag(self, val, neg, c):
|
||||
def _findTag(self, val, args):
|
||||
if val == "none":
|
||||
if neg:
|
||||
t = "tags != ''"
|
||||
else:
|
||||
t = "tags = ''"
|
||||
self.lims['preds'].append(t)
|
||||
return
|
||||
extra = "not" if neg else ""
|
||||
return 'tags = ""'
|
||||
val = val.replace("*", "%")
|
||||
if not val.startswith("%"):
|
||||
val = "% " + val
|
||||
if not val.endswith("%"):
|
||||
val += " %"
|
||||
self.lims['args']["_tag_%d" % c] = val
|
||||
self.lims['preds'].append(
|
||||
"tags %s like :_tag_%d""" % (extra, c))
|
||||
args.append(val)
|
||||
return "n.tags like ?"
|
||||
|
||||
def _findCardState(self, val, neg):
|
||||
cond = None
|
||||
def _findCardState(self, val):
|
||||
if val in ("review", "new", "learn"):
|
||||
if val == "review":
|
||||
n = 2
|
||||
|
@ -155,40 +230,33 @@ and c.nid=n.id %s""" % (q, order)
|
|||
n = 0
|
||||
else:
|
||||
n = 1
|
||||
cond = "type = %d" % n
|
||||
return "type = %d" % n
|
||||
elif val == "suspended":
|
||||
cond = "queue = -1"
|
||||
return "c.queue = -1"
|
||||
elif val == "due":
|
||||
cond = "(queue = 2 and due <= %d)" % self.col.sched.today
|
||||
if cond:
|
||||
if neg:
|
||||
cond = "not (%s)" % cond
|
||||
self.lims['preds'].append(cond)
|
||||
else:
|
||||
self.lims['valid'] = False
|
||||
return """
|
||||
(c.queue in (2,3) and c.due <= %d) or
|
||||
(c.queue = 1 and c.due <= %d)""" % (
|
||||
self.col.sched.today, self.col.sched.dayCutoff)
|
||||
|
||||
def _findRated(self, val, neg):
|
||||
def _findRated(self, val):
|
||||
r = val.split(":")
|
||||
if len(r) != 2 or r[0] not in ("1", "2", "3", "4"):
|
||||
self.lims['valid'] = False
|
||||
return
|
||||
try:
|
||||
days = int(r[1])
|
||||
except ValueError:
|
||||
self.lims['valid'] = False
|
||||
return
|
||||
# bound the search
|
||||
days = min(days, 31)
|
||||
lim = self.col.sched.dayCutoff - 86400*days
|
||||
self.lims['preds'].append(
|
||||
"c.id in (select cid from revlog where ease=%s and id>%d)" %
|
||||
(r[0], (lim*1000)))
|
||||
return ("c.id in (select cid from revlog where ease=%s and id>%d)" %
|
||||
(r[0], (lim*1000)))
|
||||
|
||||
def _findProp(self, val, neg):
|
||||
def _findProp(self, val):
|
||||
# extract
|
||||
m = re.match("(^.+?)(<=|>=|=|<|>)(.+?$)", val)
|
||||
if not m:
|
||||
self.lims['valid'] = False
|
||||
return
|
||||
prop, cmp, val = m.groups()
|
||||
prop = prop.lower()
|
||||
|
@ -199,134 +267,90 @@ and c.nid=n.id %s""" % (q, order)
|
|||
else:
|
||||
val = int(val)
|
||||
except ValueError:
|
||||
self.lims['valid'] = False
|
||||
return
|
||||
# is prop valid?
|
||||
if prop not in ("due", "ivl", "reps", "lapses", "ease"):
|
||||
self.lims['valid'] = False
|
||||
return
|
||||
# query
|
||||
extra = "not" if neg else ""
|
||||
q = []
|
||||
if prop == "due":
|
||||
val += self.col.sched.today
|
||||
# only valid for review/daily learning
|
||||
self.lims['preds'].append("queue in (2,3)")
|
||||
q.append("(c.queue in (2,3))")
|
||||
elif prop == "ease":
|
||||
prop = "factor"
|
||||
val = int(val*1000)
|
||||
sql = "%s (%s %s %s)" % ("not" if neg else "",
|
||||
prop, cmp, val)
|
||||
self.lims['preds'].append(sql)
|
||||
q.append("(%s %s %s)" % (prop, cmp, val))
|
||||
return " and ".join(q)
|
||||
|
||||
def _findText(self, val, neg, c):
|
||||
def _findText(self, val, args):
|
||||
val = val.replace("*", "%")
|
||||
if not self.full:
|
||||
self.lims['args']["_text_%d"%c] = "%"+val+"%"
|
||||
txt = """
|
||||
(sfld like :_text_%d escape '\\' or flds like :_text_%d escape '\\')""" % (c,c)
|
||||
if not neg:
|
||||
self.lims['preds'].append(txt)
|
||||
else:
|
||||
self.lims['preds'].append("not " + txt)
|
||||
else:
|
||||
nids = []
|
||||
extra = "not" if neg else ""
|
||||
for nid, flds in self.col.db.execute(
|
||||
"select id, flds from notes"):
|
||||
if val in stripHTML(flds):
|
||||
nids.append(nid)
|
||||
self.lims['preds'].append("n.id %s in %s " % (extra, ids2str(nids)))
|
||||
args.append("%"+val+"%")
|
||||
args.append("%"+val+"%")
|
||||
return "(n.sfld like ? escape '\\' or n.flds like ? escape '\\')"
|
||||
|
||||
def _findNids(self, val):
|
||||
self.lims['preds'].append("n.id in (%s)" % val)
|
||||
if re.search("[^0-9,]", val):
|
||||
return
|
||||
return "n.id in (%s)" % val
|
||||
|
||||
def _findModel(self, val, isNeg):
|
||||
extra = "not" if isNeg else ""
|
||||
def _findModel(self, val):
|
||||
ids = []
|
||||
for m in self.col.models.all():
|
||||
if m['name'].lower() == val:
|
||||
ids.append(m['id'])
|
||||
self.lims['preds'].append("mid %s in %s" % (extra, ids2str(ids)))
|
||||
return "n.mid in %s" % ids2str(ids)
|
||||
|
||||
def _findDeck(self, val, isNeg):
|
||||
ids = []
|
||||
def _findDeck(self, val):
|
||||
def dids(did):
|
||||
if not did:
|
||||
return None
|
||||
return [did] + [a[1] for a in self.col.decks.children(did)]
|
||||
# current deck?
|
||||
ids = None
|
||||
if val.lower() == "current":
|
||||
id = self.col.decks.current()['id']
|
||||
elif val.lower() == "none":
|
||||
if isNeg:
|
||||
extra = ""
|
||||
else:
|
||||
extra = "not"
|
||||
self.lims['preds'].append(
|
||||
"c.did %s in %s" % (extra, ids2str(self.col.decks.allIds())))
|
||||
return
|
||||
ids = dids(self.col.decks.current()['id'])
|
||||
elif "*" not in val:
|
||||
# single deck
|
||||
id = self.col.decks.id(val, create=False) or 0
|
||||
ids = dids(self.col.decks.id(val, create=False))
|
||||
else:
|
||||
# wildcard
|
||||
ids = set()
|
||||
val = val.replace("*", ".*")
|
||||
for d in self.col.decks.all():
|
||||
if re.match("(?i)"+val, d['name']):
|
||||
id = d['id']
|
||||
ids.extend([id] + [
|
||||
a[1] for a in self.col.decks.children(id)])
|
||||
if not ids:
|
||||
# invalid search
|
||||
self.lims['valid'] = False
|
||||
return
|
||||
ids.update(dids(d['id']))
|
||||
if not ids:
|
||||
ids = [id] + [a[1] for a in self.col.decks.children(id)]
|
||||
return
|
||||
sids = ids2str(ids)
|
||||
if not isNeg:
|
||||
# normal search
|
||||
self.lims['preds'].append(
|
||||
"(c.odid in %s or c.did in %s)" % (sids, sids))
|
||||
else:
|
||||
# inverted search
|
||||
self.lims['preds'].append("""
|
||||
((case c.odid when 0 then 1 else c.odid not in %s end) and c.did not in %s)
|
||||
""" % (sids, sids))
|
||||
return "c.did in %s or c.odid in %s" % (sids, sids)
|
||||
|
||||
def _findTemplate(self, val, isNeg):
|
||||
lims = []
|
||||
comp = "!=" if isNeg else "="
|
||||
found = False
|
||||
def _findTemplate(self, val):
|
||||
# were we given an ordinal number?
|
||||
try:
|
||||
num = int(val) - 1
|
||||
except:
|
||||
num = None
|
||||
lims = []
|
||||
# were we given an ordinal number?
|
||||
if num is not None:
|
||||
found = True
|
||||
self.lims['preds'].append("ord %s %d" % (comp, num))
|
||||
else:
|
||||
# search for template names
|
||||
for m in self.col.models.all():
|
||||
for t in m['tmpls']:
|
||||
# template name?
|
||||
if t['name'].lower() == val.lower():
|
||||
if m['type'] == MODEL_CLOZE:
|
||||
# if the user has asked for a cloze card, we want
|
||||
# to give all ordinals, so we just limit to the
|
||||
# model instead
|
||||
lims.append("(mid = %s)" % m['id'])
|
||||
found = True
|
||||
else:
|
||||
lims.append((
|
||||
"(nid in (select id from notes where mid = %s) "
|
||||
"and ord %s %d)") % (m['id'], comp, t['ord']))
|
||||
found = True
|
||||
if lims:
|
||||
self.lims['preds'].append("(" + " or ".join(lims) + ")")
|
||||
self.lims['valid'] = found
|
||||
return "c.ord = %d" % num
|
||||
# search for template names
|
||||
lims = []
|
||||
for m in self.col.models.all():
|
||||
for t in m['tmpls']:
|
||||
if t['name'].lower() == val.lower():
|
||||
if m['type'] == MODEL_CLOZE:
|
||||
# if the user has asked for a cloze card, we want
|
||||
# to give all ordinals, so we just limit to the
|
||||
# model instead
|
||||
lims.append("(n.mid = %s)" % m['id'])
|
||||
else:
|
||||
lims.append("(n.mid = %s and c.ord = %s)" % (
|
||||
m['id'], t['ord']))
|
||||
return " or ".join(lims)
|
||||
|
||||
def _findField(self, token, isNeg):
|
||||
field = value = ''
|
||||
parts = token.split(':', 1);
|
||||
field = parts[0].lower()
|
||||
value = "%" + parts[1].replace("*", "%") + "%"
|
||||
def _findField(self, field, val):
|
||||
field = field.lower()
|
||||
val = val.replace("*", "%")
|
||||
# find models that have that field
|
||||
mods = {}
|
||||
for m in self.col.models.all():
|
||||
|
@ -335,146 +359,23 @@ and c.nid=n.id %s""" % (q, order)
|
|||
mods[str(m['id'])] = (m, f['ord'])
|
||||
if not mods:
|
||||
# nothing has that field
|
||||
self.lims['valid'] = False
|
||||
return
|
||||
# gather nids
|
||||
regex = value.replace("_", ".").replace("%", ".*")
|
||||
regex = val.replace("_", ".").replace("%", ".*")
|
||||
nids = []
|
||||
for (id,mid,flds) in self.col.db.execute("""
|
||||
select id, mid, flds from notes
|
||||
where mid in %s and flds like ? escape '\\'""" % (
|
||||
ids2str(mods.keys())),
|
||||
"%" if self.full else value):
|
||||
"%"+val+"%"):
|
||||
flds = splitFields(flds)
|
||||
ord = mods[str(mid)][1]
|
||||
strg = flds[ord]
|
||||
if self.full:
|
||||
strg = stripHTML(strg)
|
||||
if re.search("(?i)"+regex, strg):
|
||||
if re.search("(?i)^"+regex+"$", strg):
|
||||
nids.append(id)
|
||||
extra = "not" if isNeg else ""
|
||||
self.lims['preds'].append("""
|
||||
n.mid in %s and n.id %s in %s""" % (
|
||||
ids2str(mods.keys()), extra, ids2str(nids)))
|
||||
|
||||
# Most of this function was written by Marcus
|
||||
def _parseQuery(self):
|
||||
tokens = []
|
||||
res = []
|
||||
allowedfields = fieldNames(self.col)
|
||||
def addSearchFieldToken(field, value, isNeg):
|
||||
if field.lower() in allowedfields:
|
||||
res.append((field + ':' + value, isNeg, SEARCH_FIELD))
|
||||
else:
|
||||
for p in phraselog:
|
||||
res.append((p['value'], p['is_neg'], p['type']))
|
||||
# break query into words or phraselog
|
||||
# an extra space is added so the loop never ends in the middle
|
||||
# completing a token
|
||||
for match in re.findall(
|
||||
r'(-)?\'(([^\'\\]|\\.)*)\'|(-)?"(([^"\\]|\\.)*)"|(-)?([^ ]+)|([ ]+)',
|
||||
self.query + ' '):
|
||||
value = (match[1] or match[4] or match[7])
|
||||
isNeg = (match[0] == '-' or match[3] == '-' or match[6] == '-')
|
||||
tokens.append({'value': value, 'is_neg': isNeg})
|
||||
intoken = isNeg = False
|
||||
field = '' #name of the field for field related commands
|
||||
phraselog = [] #log of phrases in case potential command is not a commad
|
||||
for c, token in enumerate(tokens):
|
||||
doprocess = True # only look for commands when this is true
|
||||
#prevent cases such as "field" : value as being processed as a command
|
||||
if len(token['value']) == 0:
|
||||
if intoken is True and type == SEARCH_FIELD and field:
|
||||
#case: fieldname: any thing here check for existance of fieldname
|
||||
addSearchFieldToken(field, '*', isNeg)
|
||||
phraselog = [] # reset phrases since command is completed
|
||||
intoken = doprocess = False
|
||||
if intoken is True:
|
||||
if type == SEARCH_FIELD and field:
|
||||
#case: fieldname:"value"
|
||||
addSearchFieldToken(field, token['value'], isNeg)
|
||||
intoken = doprocess = False
|
||||
elif type == SEARCH_FIELD and not field:
|
||||
#case: "fieldname":"name" or "field" anything
|
||||
if token['value'].startswith(":") and len(phraselog) == 1:
|
||||
#we now know a colon is next, so mark it as field
|
||||
# and keep looking for the value
|
||||
field = phraselog[0]['value']
|
||||
parts = token['value'].split(':', 1)
|
||||
phraselog.append(
|
||||
{'value': token['value'], 'is_neg': False,
|
||||
'type': SEARCH_PHRASE})
|
||||
if parts[1]:
|
||||
#value is included with the :, so wrap it up
|
||||
addSearchFieldToken(field, parts[1], isNeg)
|
||||
intoken = doprocess = False
|
||||
doprocess = False
|
||||
else:
|
||||
#case: "fieldname"string/"fieldname"tag:name
|
||||
intoken = False
|
||||
if intoken is False and doprocess is False:
|
||||
#command has been fully processed
|
||||
phraselog = [] # reset phraselog, since we used it for a command
|
||||
if intoken is False:
|
||||
#include any non-command related phrases in the query
|
||||
for p in phraselog: res.append(
|
||||
(p['value'], p['is_neg'], p['type']))
|
||||
phraselog = []
|
||||
if intoken is False and doprocess is True:
|
||||
field = ''
|
||||
isNeg = token['is_neg']
|
||||
if token['value'].startswith("tag:"):
|
||||
token['value'] = token['value'][4:]
|
||||
type = SEARCH_TAG
|
||||
elif token['value'].startswith("is:"):
|
||||
token['value'] = token['value'][3:].lower()
|
||||
type = SEARCH_TYPE
|
||||
elif token['value'].startswith("note:"):
|
||||
token['value'] = token['value'][5:].lower()
|
||||
type = SEARCH_MODEL
|
||||
elif token['value'].startswith("deck:"):
|
||||
token['value'] = token['value'][5:].lower()
|
||||
type = SEARCH_DECK
|
||||
elif token['value'].startswith("prop:"):
|
||||
token['value'] = token['value'][5:].lower()
|
||||
type = SEARCH_PROP
|
||||
elif token['value'].startswith("rated:"):
|
||||
token['value'] = token['value'][6:].lower()
|
||||
type = SEARCH_RATED
|
||||
elif token['value'].startswith("nid:") and len(token['value']) > 4:
|
||||
dec = token['value'][4:]
|
||||
try:
|
||||
int(dec)
|
||||
token['value'] = token['value'][4:]
|
||||
except:
|
||||
try:
|
||||
for d in dec.split(","):
|
||||
int(d)
|
||||
token['value'] = token['value'][4:]
|
||||
except:
|
||||
token['value'] = "0"
|
||||
type = SEARCH_NID
|
||||
elif token['value'].startswith("card:"):
|
||||
token['value'] = token['value'][5:]
|
||||
type = SEARCH_TEMPLATE
|
||||
else:
|
||||
type = SEARCH_FIELD
|
||||
intoken = True
|
||||
parts = token['value'].split(':', 1)
|
||||
phraselog.append(
|
||||
{'value': token['value'], 'is_neg': isNeg,
|
||||
'type': SEARCH_PHRASE})
|
||||
if len(parts) == 2 and parts[0]:
|
||||
field = parts[0]
|
||||
if parts[1]:
|
||||
#simple fieldname:value case -
|
||||
#no need to look for more data
|
||||
addSearchFieldToken(field, parts[1], isNeg)
|
||||
intoken = doprocess = False
|
||||
if intoken is False: phraselog = []
|
||||
if intoken is False and doprocess is True:
|
||||
res.append((token['value'], isNeg, type))
|
||||
return res
|
||||
if not nids:
|
||||
return
|
||||
return "n.id in %s" % ids2str(nids)
|
||||
|
||||
# Find and replace
|
||||
##########################################################################
|
||||
|
@ -522,11 +423,24 @@ def findReplace(col, nids, src, dst, regex=False, field=None, fold=True):
|
|||
if not d:
|
||||
return 0
|
||||
# replace
|
||||
col.db.executemany("update notes set flds=:flds,mod=:m,usn=:u where id=:nid", d)
|
||||
col.db.executemany(
|
||||
"update notes set flds=:flds,mod=:m,usn=:u where id=:nid", d)
|
||||
col.updateFieldCache(nids)
|
||||
col.genCards(nids)
|
||||
return len(d)
|
||||
|
||||
def fieldNames(col, downcase=True):
|
||||
fields = set()
|
||||
names = []
|
||||
for m in col.models.all():
|
||||
for f in m['flds']:
|
||||
if f['name'].lower() not in fields:
|
||||
names.append(f['name'])
|
||||
fields.add(f['name'].lower())
|
||||
if downcase:
|
||||
return list(fields)
|
||||
return names
|
||||
|
||||
# Find duplicates
|
||||
##########################################################################
|
||||
|
||||
|
|
|
@ -880,23 +880,23 @@ due = odue, odue = 0, odid = 0, usn = ?, mod = ? where %s""" % lim,
|
|||
def _dynOrder(self, deck):
|
||||
o = deck['order']
|
||||
if o == DYN_OLDEST:
|
||||
return "order by c.mod"
|
||||
return "c.mod"
|
||||
elif o == DYN_RANDOM:
|
||||
return "order by random()"
|
||||
return "random()"
|
||||
elif o == DYN_SMALLINT:
|
||||
return "order by ivl"
|
||||
return "ivl"
|
||||
elif o == DYN_BIGINT:
|
||||
return "order by ivl desc"
|
||||
return "ivl desc"
|
||||
elif o == DYN_LAPSES:
|
||||
return "order by lapses desc"
|
||||
elif o == DYN_FAILED:
|
||||
return """
|
||||
and c.id in (select cid from revlog where ease = 1 and time > %d)
|
||||
order by c.mod""" % ((self.dayCutoff-86400)*1000)
|
||||
return "lapses desc"
|
||||
# elif o == DYN_FAILED:
|
||||
# return """
|
||||
# and c.id in (select cid from revlog where ease = 1 and time > %d)
|
||||
# order by c.mod""" % ((self.dayCutoff-86400)*1000)
|
||||
elif o == DYN_ADDED:
|
||||
return "order by n.id"
|
||||
return "n.id"
|
||||
elif o == DYN_DUE:
|
||||
return "order by c.due"
|
||||
return "c.due"
|
||||
|
||||
def _moveToDyn(self, did, ids):
|
||||
deck = self.col.decks.get(did)
|
||||
|
|
|
@ -1,7 +1,23 @@
|
|||
# coding: utf-8
|
||||
|
||||
from anki.find import Finder
|
||||
from tests.shared import getEmptyDeck
|
||||
|
||||
def test_parse():
|
||||
f = Finder(None)
|
||||
assert f._tokenize("hello world") == ["hello", "world"]
|
||||
assert f._tokenize("hello world") == ["hello", "world"]
|
||||
assert f._tokenize("one -two") == ["one", "not", "two"]
|
||||
assert f._tokenize("one --two") == ["one", "not", "two"]
|
||||
assert f._tokenize("one or -two") == ["one", "or", "not", "two"]
|
||||
assert f._tokenize("'hello \"world\"'") == ["hello \"world\""]
|
||||
assert f._tokenize('"hello world"') == ["hello world"]
|
||||
assert f._tokenize("one (two or ( three or four))") == [
|
||||
"one", "(", "two", "or", "(", "three", "or", "four",
|
||||
")", ")"]
|
||||
assert f._tokenize("embedded'string") == ["embedded'string"]
|
||||
assert f._tokenize("deck:'two words'") == ["deck:two words"]
|
||||
|
||||
def test_findCards():
|
||||
deck = getEmptyDeck()
|
||||
f = deck.newNote()
|
||||
|
@ -29,7 +45,7 @@ def test_findCards():
|
|||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
f = deck.newNote()
|
||||
f['Front'] = u'template test'
|
||||
f['Front'] = u'test'
|
||||
f['Back'] = u'foo bar'
|
||||
deck.addNote(f)
|
||||
latestCardIds = [c.id for c in f.cards()]
|
||||
|
@ -87,19 +103,20 @@ def test_findCards():
|
|||
assert len(deck.findCards("front:sheep")) == 0
|
||||
assert len(deck.findCards("back:sheep")) == 2
|
||||
assert len(deck.findCards("-back:sheep")) == 3
|
||||
assert len(deck.findCards("front:")) == 5
|
||||
assert len(deck.findCards("front:do")) == 0
|
||||
assert len(deck.findCards("front:*")) == 5
|
||||
# ordering
|
||||
deck.conf['sortType'] = "noteCrt"
|
||||
assert deck.findCards("front:")[-1] in latestCardIds
|
||||
assert deck.findCards("")[-1] in latestCardIds
|
||||
assert deck.findCards("front:*", order=True)[-1] in latestCardIds
|
||||
assert deck.findCards("", order=True)[-1] in latestCardIds
|
||||
deck.conf['sortType'] = "noteFld"
|
||||
assert deck.findCards("")[0] == catCard.id
|
||||
assert deck.findCards("")[-1] in latestCardIds
|
||||
assert deck.findCards("", order=True)[0] == catCard.id
|
||||
assert deck.findCards("", order=True)[-1] in latestCardIds
|
||||
deck.conf['sortType'] = "cardMod"
|
||||
assert deck.findCards("")[-1] in latestCardIds
|
||||
assert deck.findCards("")[0] == firstCardId
|
||||
assert deck.findCards("", order=True)[-1] in latestCardIds
|
||||
assert deck.findCards("", order=True)[0] == firstCardId
|
||||
deck.conf['sortBackwards'] = True
|
||||
assert deck.findCards("")[0] in latestCardIds
|
||||
assert deck.findCards("", order=True)[0] in latestCardIds
|
||||
# model
|
||||
assert len(deck.findCards("note:basic")) == 5
|
||||
assert len(deck.findCards("-note:basic")) == 0
|
||||
|
@ -118,14 +135,13 @@ def test_findCards():
|
|||
deck.addNote(f)
|
||||
# as it's the sort field, it matches
|
||||
assert len(deck.findCards("helloworld")) == 2
|
||||
assert len(deck.findCards("helloworld", full=True)) == 2
|
||||
#assert len(deck.findCards("helloworld", full=True)) == 2
|
||||
# if we put it on the back, it won't
|
||||
(f['Front'], f['Back']) = (f['Back'], f['Front'])
|
||||
f.flush()
|
||||
assert len(deck.findCards("helloworld")) == 0
|
||||
assert len(deck.findCards("helloworld", full=True)) == 2
|
||||
assert len(deck.findCards("front:helloworld")) == 0
|
||||
assert len(deck.findCards("back:helloworld", full=True)) == 2
|
||||
#assert len(deck.findCards("helloworld", full=True)) == 2
|
||||
#assert len(deck.findCards("back:helloworld", full=True)) == 2
|
||||
# searching for an invalid special tag should not error
|
||||
assert len(deck.findCards("is:invalid")) == 0
|
||||
# should be able to limit to parent deck, no children
|
||||
|
@ -173,6 +189,18 @@ def test_findCards():
|
|||
assert len(deck.findCards("rated:2:1")) == 1
|
||||
assert len(deck.findCards("rated:2:0")) == 0
|
||||
assert len(deck.findCards("rated:2:2")) == 1
|
||||
# empty field
|
||||
assert len(deck.findCards("front:")) == 0
|
||||
f = deck.newNote()
|
||||
f['Front'] = u''
|
||||
f['Back'] = u'abc2'
|
||||
assert deck.addNote(f) == 1
|
||||
assert len(deck.findCards("front:")) == 1
|
||||
# OR searches and nesting
|
||||
assert len(deck.findCards("tag:monkey or tag:sheep")) == 2
|
||||
assert len(deck.findCards("(tag:monkey OR tag:sheep)")) == 2
|
||||
assert len(deck.findCards("tag:monkey or (tag:sheep sheep)")) == 2
|
||||
assert len(deck.findCards("tag:monkey or (tag:sheep octopus)")) == 1
|
||||
|
||||
def test_findReplace():
|
||||
deck = getEmptyDeck()
|
||||
|
|
Loading…
Reference in a new issue