add mail search

This commit is contained in:
Frédéric Péters 2011-12-06 17:17:15 +01:00
parent d787ee4b4c
commit 3dc65a9770
10 changed files with 607 additions and 3 deletions

View File

@ -0,0 +1,167 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="plone">
<body>
<tal:comment condition="nothing">You can feed in batch_base_url by enclosing
the metal:use-macro="context/batch_macros/macros/navigation" statement in your
template with a tal:define="batch_base_url YOUR_BASE_URL" tales expression.
</tal:comment>
<div id="results-nav" class="listingBar"
metal:define-macro="navigation"
tal:define="request request|context/request|container/request|nothing;
batch batch|nothing;
batchformkeys batchformkeys|nothing;
batchlinkparams view/get_batchlinkparams;
mq python:modules['ZTUtils'].make_query;
url batch_base_url | request/ACTUAL_URL;
currentpage batch/pagenumber;"
tal:condition="python: batch.next or batch.previous">
<tal:block tal:define="p batch/previous | nothing">
<a href="" id="results-nav-prev" tal:condition="p"
tal:attributes="href python: '%s?%s#%s' % (url , mq( batchlinkparams, {batch.b_start_str:p.first} ), batch.b_start_str)">
&laquo;
<span i18n:translate="batch_previous_x_items" tal:omit-tag="">
Previous
<span i18n:name="number" tal:omit-tag="" tal:content="p/length">n</span>
items
</span>
</a>
<span id="results-nav-prev" tal:condition="not: p">
&laquo;
<span i18n:translate="batch_previous_x_items" tal:omit-tag="">
Previous
<span i18n:name="number" tal:omit-tag="" tal:content="b_size">n</span>
items
</span>
</span>
</tal:block>
<tal:block tal:define="n batch/next | nothing">
<a href="" id="results-nav-next" tal:condition="n"
tal:attributes="href python: '%s?%s#%s' % (url , mq( batchlinkparams, {batch.b_start_str:n.first} ), batch.b_start_str)">
<span i18n:translate="batch_next_x_items" tal:omit-tag="">
Next
<span i18n:name="number" tal:omit-tag="" tal:content="n/length">n</span>
items
</span>
&raquo;
</a>
<span id="results-nav-next" tal:condition="not: n">
<span i18n:translate="batch_next_x_items" tal:omit-tag="">
Next
<span i18n:name="number" tal:omit-tag="" tal:content="b_size">n</span>
items
</span>
&raquo;
</span>
</tal:block>
<span class="pager">Page : </span>
<tal:comment tal:condition="nothing">
Link to first
</tal:comment>
<span tal:condition="python: 1 not in batch.navlist">
<a href=""
tal:attributes="href python: '%s?%s#%s' % (url, batch.pageurl(batchlinkparams,1), batch.b_start_str);">1</a>
<span tal:condition="python: 2 not in (batch.prevlist or batch.leapback)"
tal:omit-tag="">
&hellip;
</span>
</span>
<tal:comment tal:condition="nothing">
Pagelist with quantum leap links to previous pages for quick navigation
</tal:comment>
<span tal:repeat="linklist python:batch.navurls(batchlinkparams, batch.leapback)"
tal:condition="batch/leapback"
tal:omit-tag="" >
<a href=""
tal:define="page python:linklist[0];
query python:linklist[1];"
tal:content="page"
tal:attributes="href python: '%s?%s#%s' % (url,query, batch.b_start_str)" >
</a>
&hellip;
</span>
<tal:comment tal:condition="nothing">
Pagelist with links to previous pages for quick navigation
</tal:comment>
<span tal:repeat="linklist python:batch.prevurls(batchlinkparams)"
tal:condition="batch/prevlist"
tal:omit-tag="" >
<a href=""
tal:define="page python:linklist[0];
query python:linklist[1];"
tal:content="page"
tal:attributes="href python: '%s?%s#%s' % (url,query, batch.b_start_str)" >
</a>
</span>
<tal:comment tal:replace="nothing">
Current page
</tal:comment>
<span tal:condition="batch/navlist" class="current"
tal:content="batch/pagenumber">Current page number</span>
<tal:comment tal:replace="nothing">
Pagelist with links to next pages for quick navigation
</tal:comment>
<span tal:repeat="linklist python:batch.nexturls(batchlinkparams)"
tal:condition="batch/nextlist"
tal:omit-tag="" >
<a href=""
tal:define="page python:linklist[0];
query python:linklist[1];"
tal:content="page"
tal:attributes="href python: '%s?%s#%s' % (url,query, batch.b_start_str)" >
</a>
</span>
<tal:comment tal:replace="nothing">
Pagelist with quantum leap links to next pages for quick navigation
</tal:comment>
<span tal:repeat="linklist python:batch.navurls(batchlinkparams, batch.leapforward)"
tal:condition="batch/leapforward"
tal:omit-tag="" >
&hellip;
<a href=""
tal:define="page python:linklist[0];
query python:linklist[1];"
tal:content="page"
tal:attributes="href python: '%s?%s#%s' % (url,query,batch.b_start_str)" >
</a>
</span>
<tal:comment tal:replace="nothing">
Link to last
</tal:comment>
<span tal:condition="python:batch.numpages not in batch.navlist">
<span tal:condition="python: batch.numpages - 1 not in (batch.nextlist or batch.leapforward)"
tal:omit-tag="">
&hellip;
</span>
<a href=""
tal:attributes="href python: '%s?%s#%s' % (url,batch.pageurl(batchlinkparams,batch.numpages), batch.b_start_str);"
tal:content="batch/numpages">3457</a>
</span>
</div>
<div id="results-feed"
metal:define-macro="feed"
tal:define="
batch batch|nothing;
batchformkeys batchformkeys|nothing;
batchlinkparams view/get_batchlinkparams;
">
<a tal:attributes="href python: 'feed?%s' % batch.pageurl(batchlinkparams,1);">Abonnez-vous au flux de cette recherche</a>
</div>
</body>
</html>

View File

@ -1,11 +1,33 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
xmlns:grok="http://namespaces.zope.org/grok"
xmlns:i18n="http://namespaces.zope.org/i18n"
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="themis.search">
<five:registerPackage package="." initialize=".initialize" />
<!-- Include z3c.form as dependency -->
<include package="plone.app.z3cform" />
<!-- Grok the package to initialise schema interfaces and content classes -->
<grok:grok package="." />
<browser:page
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
name="mailsearch"
class=".mail.SearchView"
permission="zope2.View"
template="mail.pt"/>
<!-- Register an extension profile to make the product installable -->
<genericsetup:registerProfile
name="default"
title="Themis Search"
description="..."
directory="profiles/default"
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
<!-- -*- extra stuff goes here -*- -->
</configure>

View File

@ -0,0 +1,39 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
tal:omit-tag="">
<select size="5" tal:attributes="name string:${view/name}-select">
<option tal:repeat="item view/get_known_contacts"
tal:attributes="value item/token" tal:content="item/title"/>
</select><br/>
<input id="" name="" class="" title="" lang="" disabled=""
readonly="" alt="" tabindex="" accesskey="" size="" maxlength=""
style="" value="" type="text"
tal:attributes="id view/id;
name view/name;
class view/klass;
style view/style;
title view/title;
lang view/lang;
onclick view/onclick;
ondblclick view/ondblclick;
onmousedown view/onmousedown;
onmouseup view/onmouseup;
onmouseover view/onmouseover;
onmousemove view/onmousemove;
onmouseout view/onmouseout;
onkeypress view/onkeypress;
onkeydown view/onkeydown;
onkeyup view/onkeyup;
value view/value;
disabled view/disabled;
tabindex view/tabindex;
onfocus view/onfocus;
onblur view/onblur;
onchange view/onchange;
readonly view/readonly;
alt view/alt;
accesskey view/accesskey;
onselect view/onselect;
size view/size;
maxlength view/maxlength" />
</html>

5
themis/search/doc.py Normal file
View File

@ -0,0 +1,5 @@
from Products.Five import BrowserView
class SearchView(BrowserView):
pass

108
themis/search/indexer.py Normal file
View File

@ -0,0 +1,108 @@
from DateTime import DateTime
from plone.indexer import indexer
from five import grok
import plone.dexterity.interfaces
from Products.CMFCore.utils import getToolByName
from ZODB.POSException import ConflictError
from themis.fields.vocabs import ContactsSource
@indexer(plone.dexterity.interfaces.IDexterityItem)
def dateIndexer(obj):
if obj.portal_type not in ('courrier_entrant', 'courrier_sortant'):
return None
for attr in ('date_reelle_courrier', 'date_reception'):
if not hasattr(obj, attr):
continue
if getattr(obj, attr):
return DateTime(getattr(obj, attr).isoformat())
return None
grok.global_adapter(dateIndexer, name="dateCourrier")
@indexer(plone.dexterity.interfaces.IDexterityItem)
def mail_dynamic_searchable_text_indexer(obj):
"""Dynamic searchable text indexer.
"""
if obj.portal_type not in ('courrier_entrant', 'courrier_sortant'):
return None
data = obj.fichier
if not data or data.getSize() == 0:
return obj.title
# if there is no path to text/plain, do nothing
transforms = getToolByName(obj, 'portal_transforms')
if not transforms._findPath(data.contentType, 'text/plain'):
return obj.title
# convert it to text/plain
try:
datastream = transforms.convertTo(
'text/plain', data.data, mimetype=data.contentType,
filename=data.filename)
data = datastream.getData()
except (ConflictError, KeyboardInterrupt):
raise
try:
data = unicode(datastream.getData(), 'utf-8')
except UnicodeDecodeError, e:
try:
data = unicode(datastream.getData()[:e.start], 'utf-8')
except UnicodeDecodeError:
# ok, forget it
data = ''
return obj.title + ' ' + data
grok.global_adapter(mail_dynamic_searchable_text_indexer, name='MailSearchableText')
@indexer(plone.dexterity.interfaces.IDexterityItem)
def contactIndexer(obj):
if obj.portal_type not in ('courrier_entrant', 'courrier_sortant'):
return None
contacts_dir = getattr(getToolByName(obj, 'portal_url').getPortalObject(), 'contacts')
for attr in ('expediteur', 'destinataire'):
if not hasattr(obj, attr):
continue
v = getattr(obj, attr)
if not v:
continue
v = v[0]
if ':' in v and v.split(':')[0] in ('deputy', 'ministry', 'ministry-collab'):
return v
elif ':' in v and v.split(':')[0] == 'contact':
contact_id = v.split(':')[1]
if not hasattr(contacts_dir, contact_id):
return contact_id.replace('-', ' ')
else:
return getattr(contacts_dir, contact_id).title
else:
return v
return None
grok.global_adapter(contactIndexer, name="MailContact")
@indexer(plone.dexterity.interfaces.IDexterityItem)
def contactFuzzyIndexer(obj):
if obj.portal_type not in ('courrier_entrant', 'courrier_sortant'):
return None
for attr in ('expediteur', 'destinataire'):
if not hasattr(obj, attr):
continue
v = getattr(obj, attr)
if not v:
continue
v = v[0]
if ':' in v:
src = ContactsSource()
return src(obj).getTermByToken(v).title
else:
return v
return None
grok.global_adapter(contactFuzzyIndexer, name="MailContactFuzzy")

View File

@ -0,0 +1,4 @@
import zope.i18nmessageid
MessageFactory = zope.i18nmessageid.MessageFactory('themis.search')

69
themis/search/mail.pt Normal file
View File

@ -0,0 +1,69 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
lang="en"
metal:use-macro="context/main_template/macros/master"
i18n:domain="themis.search">
<body>
<metal:main fill-slot="main">
<tal:main-macro metal:define-macro="main">
<tal:block tal:define="Batch python:modules['Products.CMFPlone'].Batch;
mq python:modules['ZTUtils'].make_query;
url batch_base_url | request/ACTUAL_URL;
batchformkeys batchformkeys|nothing;
b_size python:50; b_size request/b_size | b_size;
b_start python:0;b_start request/b_start | b_start;
results view/search_results;
batch python:Batch(list(results), b_size, int(b_start), orphan=1);
">
<h1 class="documentFirstHeading">Recherche de courrier</h1>
<form method="post" action="mailsearch#results">
<div tal:replace="structure view/form"/>
</form>
<div id="results">
<div class="resultsinfobox">
<span>Nombre de résultats : <span tal:replace="python:len(results)"/></span>
</div>
<div class="results-var">
<div class="results">
<table>
<tal:entry tal:repeat="item batch">
<tr tal:define="oddrow repeat/item/odd;" tal:attributes="class python: oddrow and 'odd' or 'even'">
<td class="date" tal:content="string: ${item/dateCourrier/day}/${item/dateCourrier/month}/${item/dateCourrier/year}"></td>
<td><a tal:attributes="href item/getURL" tal:content="item/Title"></a></td>
</tr>
</tal:entry>
</table>
</div>
<style>
.results table tr {
line-height: 250%;
}
.results table tr.even {
background: #eee;
}
td.date {
text-align: right;
width: 7em;
padding-right: 1em;
}
</style>
<div tal:condition="python: len(results)">
<div metal:use-macro="view/batch_macros/macros/navigation" />
</div>
</tal:block>
</tal:main-macro>
</metal:main>
</body>
</html>

148
themis/search/mail.py Normal file
View File

@ -0,0 +1,148 @@
from five import grok
from zope import interface, schema, component
from zope.interface import implements
from Products.Five import BrowserView
from Products.CMFCore.utils import getToolByName
from zope.schema.interfaces import IContextSourceBinder
from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
from z3c.form import form, field, button, widget
from z3c.form.ptcompat import ViewPageTemplateFile
from z3c.form.browser.radio import RadioWidget
from z3c.form.browser.text import TextWidget
from z3c.form.interfaces import ITextWidget
import z3c.form.interfaces
from themis.fields.vocabs import get_terms_for_persons
from interfaces import MessageFactory as _
def FieldRadioboxesWidget(field, request):
return widget.FieldWidget(field, RadioWidget(request))
class IContactWidget(ITextWidget):
pass
class ContactWidget(TextWidget):
implements(IContactWidget)
input_template = ViewPageTemplateFile('contact-input.pt')
def render(self):
if self.mode == z3c.form.interfaces.INPUT_MODE:
return self.input_template(self)
return TextWidget.render(self)
def extract(self, default=z3c.form.interfaces.NO_VALUE):
t = self.request.get(self.name, default)
if not t:
t = self.request.get(self.name + '-select', default)
return t
def get_known_contacts(self):
terms = get_terms_for_persons(self.context, include_deputies=True,
include_ministries=True, include_ministries_collaborators=True)
return terms
def FieldContactWidget(field, request):
return widget.FieldWidget(field, ContactWidget(request))
@grok.provider(IContextSourceBinder)
def possible_scopes(context):
return SimpleVocabulary([
SimpleVocabulary.createTerm('all', 'all', _(u'All Mails')),
SimpleVocabulary.createTerm('incoming', 'incoming', _(u'Incoming Mails')),
SimpleVocabulary.createTerm('outgoing', 'outgoing', _(u'Outgoing Mails'))])
@grok.provider(IContextSourceBinder)
def possible_categories(context):
portal = getToolByName(context, 'portal_url').getPortalObject()
fti = getattr(portal.portal_types, 'courrier_entrant')
return fti.lookupSchema().get('categorie_de_courrier').value_type.vocabulary
@grok.provider(IContextSourceBinder)
def possible_subcategories(context):
portal = getToolByName(context, 'portal_url').getPortalObject()
fti = getattr(portal.portal_types, 'courrier_entrant')
return fti.lookupSchema().get('sous_categorie_de_courrier').value_type.vocabulary
class IMailSearchForm(interface.Interface):
text = schema.TextLine(title=_(u'Text'), required=False)
date = schema.Date(title=_(u'Date'), required=False)
number = schema.TextLine(title=_(u'Mail Number'), required=False)
category = schema.Choice(title=_(u'Category'), required=False,
source=possible_categories)
subcategory = schema.Choice(title=_(u'Subcategory'), required=False,
source=possible_subcategories)
scope = schema.Choice(title=_(u'Scope'), required=False,
source=possible_scopes, default='all')
contact = schema.TextLine(title=_(u'Contact'), required=False)
class MailSearchForm(form.Form):
fields = field.Fields(IMailSearchForm)
fields['scope'].widgetFactory = FieldRadioboxesWidget
fields['contact'].widgetFactory = FieldContactWidget
ignoreContext = True
template = ViewPageTemplateFile('view_form.pt')
@button.buttonAndHandler(_(u'Submit'))
def handleApply(self, action):
pass
class SearchView(BrowserView):
batch_macros = ViewPageTemplateFile('batch_macros.pt')
def form(self):
f = MailSearchForm(self.context, self.request)
f.update()
return f.render()
def search_results(self):
f = MailSearchForm(self.context, self.request)
f.update()
data, errors = f.extractData()
kw = {}
if not data.get('scope'):
return []
if data.get('date'):
kw['dateCourrier'] = {'query': [data.get('date'), data.get('date')], 'range': 'minmax'}
if data.get('text'):
kw['MailSearchableText'] = data.get('text')
if data.get('number'):
kw['numero_courrier'] = data.get('number')
if data.get('category'):
kw['categorie_de_courrier'] = data.get('category')
if data.get('subcategory'):
kw['sous_categorie_de_courrier'] = data.get('subcategory')
if data.get('contact'):
v = data.get('contact')
if ':' in v and v.split(':')[0] in ('deputy', 'ministry', 'ministry-collab'):
kw['MailContact'] = v
else:
kw['MailContactFuzzy'] = v
if data.get('scope') == 'all':
kw['portal_type'] = ['courrier_entrant', 'courrier_sortant']
elif data.get('scope') == 'incoming':
kw['portal_type'] = ['courrier_entrant']
elif data.get('scope') == 'outgoing':
kw['portal_type'] = ['courrier_sortant']
kw['sort_on'] = 'dateCourrier'
kw['sort_order'] = 'descending'
print 'kw:', kw
catalog = getToolByName(self.context, 'portal_catalog')
return catalog(**kw)
def get_batchlinkparams(self):
d = dict()
for key in self.request.form:
d[key] = self.request.form[key]
if type(d[key]) is str:
d[key] = unicode(d[key], 'utf-8').encode('utf-8')
elif type(d[key]) is unicode:
d[key] = d[key].encode('utf-8')
return d

View File

@ -0,0 +1,34 @@
<?xml version="1.0"?>
<object name="portal_catalog">
<object name="themis_lexicon" meta_type="ZCTextIndex Unicode Lexicon">
<element name="Case normalizer" group="Case Normalizer"/>
<element name="HTML aware splitter" group="Word Splitter"/>
<element name="Normalize accented chars (Latin &amp; Western European text)" group="Accent Normalizer"/>
</object>
<index name="themis_mail_number" meta_type="FieldIndex">
<indexed_attr value="numero_courrier"/>
</index>
<index name="categorie_de_courrier" meta_type="KeywordIndex">
<indexed_attr value="categorie_de_courrier"/>
</index>
<index name="sous_categorie_de_courrier" meta_type="KeywordIndex">
<indexed_attr value="sous_categorie_de_courrier"/>
</index>
<index name="dateCourrier" meta_type="DateIndex">
<indexed_attr value="dateCourrier"/>
</index>
<index name="MailContact" meta_type="FieldIndex">
<indexed_attr value="MailContact"/>
</index>
<index name="MailContactFuzzy" meta_type="ZCTextIndex">
<indexed_attr value="MailContactFuzzy"/>
<extra name="index_type" value="Okapi BM25 Rank"/>
<extra name="lexicon_id" value="themis_lexicon"/>
</index>
<index name="MailSearchableText" meta_type="ZCTextIndex">
<indexed_attr value="MailSearchableText"/>
<extra name="index_type" value="Okapi BM25 Rank"/>
<extra name="lexicon_id" value="themis_lexicon"/>
</index>
<column value="dateCourrier"/>
</object>

View File

@ -0,0 +1,8 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<body>
<metal:use use-macro="context/@@ploneform-macros/fields" />
<metal:use use-macro="context/@@ploneform-macros/actions" />
</body>
</html>