# -*- coding: utf-8 -*-"""Set of base controllers, which are directly plugged into the applicationobject to handle publication.:organization: Logilab:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__docformat__="restructuredtext en"fromsmtplibimportSMTPimportsimplejsonfromlogilab.common.decoratorsimportcachedfromlogilab.mtconverterimporthtml_escapefromcubicwebimportNoSelectableObject,ValidationError,ObjectNotFound,typed_eidfromcubicweb.utilsimportstrptimefromcubicweb.selectorsimportyes,match_user_groupsfromcubicweb.viewimportSTRICT_DOCTYPE,STRICT_DOCTYPE_NOEXTfromcubicweb.common.mailimportformat_mailfromcubicweb.webimportExplicitLogin,Redirect,RemoteCallFailed,json_dumpsfromcubicweb.web.controllerimportControllerfromcubicweb.web.viewsimportvid_from_rsetfromcubicweb.web.views.formrenderersimportFormRenderertry:fromcubicweb.web.facetimport(FilterRQLBuilder,get_facet,prepare_facets_rqlst)HAS_SEARCH_RESTRICTION=TrueexceptImportError:# gaeHAS_SEARCH_RESTRICTION=Falsedefxhtml_wrap(self,source):# XXX factor out, watch view.py ~ Maintemplate.doctypeifself.req.xhtml_browser():dt=STRICT_DOCTYPEelse:dt=STRICT_DOCTYPE_NOEXThead=u'<?xml version="1.0"?>\n'+dtreturnhead+u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">%s</div>'%source.strip()defjsonize(func):"""decorator to sets correct content_type and calls `simplejson.dumps` on results """defwrapper(self,*args,**kwargs):self.req.set_content_type('application/json')returnjson_dumps(func(self,*args,**kwargs))wrapper.__name__=func.__name__returnwrapperdefxhtmlize(func):"""decorator to sets correct content_type and calls `xmlize` on results"""defwrapper(self,*args,**kwargs):self.req.set_content_type(self.req.html_content_type())result=func(self,*args,**kwargs)returnxhtml_wrap(self,result)wrapper.__name__=func.__name__returnwrapperdefcheck_pageid(func):"""decorator which checks the given pageid is found in the user's session data """defwrapper(self,*args,**kwargs):data=self.req.get_session_data(self.req.pageid)ifdataisNone:raiseRemoteCallFailed(self.req._('pageid-not-found'))returnfunc(self,*args,**kwargs)returnwrapperclassLoginController(Controller):id='login'defpublish(self,rset=None):"""log in the application"""ifself.config['auth-mode']=='http':# HTTP authenticationraiseExplicitLogin()else:# Cookie authenticationreturnself.appli.need_login_content(self.req)classLogoutController(Controller):id='logout'defpublish(self,rset=None):"""logout from the application"""returnself.appli.session_handler.logout(self.req)classViewController(Controller):"""standard entry point : - build result set - select and call main template """id='view'template='main-template'defpublish(self,rset=None):"""publish a request, returning an encoded string"""view,rset=self._select_view_and_rset(rset)self.add_to_breadcrumbs(view)self.validate_cache(view)template=self.appli.main_template_id(self.req)returnself.vreg.main_template(self.req,template,rset=rset,view=view)def_select_view_and_rset(self,rset):req=self.reqifrsetisNoneandnothasattr(req,'_rql_processed'):req._rql_processed=Truerset=self.process_rql(req.form.get('rql'))ifrsetandrset.rowcount==1and'__method'inreq.form:entity=rset.get_entity(0,0)try:method=getattr(entity,req.form.pop('__method'))method()exceptRedirect:# propagate redirect that might occur in method()raiseexceptException,ex:self.exception('while handling __method')req.set_message(req._("error while handling __method: %s")%req._(ex))vid=req.form.get('vid')orvid_from_rset(req,rset,self.schema)try:view=self.vreg.select_view(vid,req,rset)exceptObjectNotFound:self.warning("the view %s could not be found",vid)req.set_message(req._("The view %s could not be found")%vid)vid=vid_from_rset(req,rset,self.schema)view=self.vreg.select_view(vid,req,rset)exceptNoSelectableObject:ifrset:req.set_message(req._("The view %s can not be applied to this query")%vid)else:req.set_message(req._("You have no access to this view or it's not applyable to current data"))self.warning("the view %s can not be applied to this query",vid)vid=vid_from_rset(req,rset,self.schema)view=self.vreg.select_view(vid,req,rset)returnview,rsetdefadd_to_breadcrumbs(self,view):# update breadcrumps **before** validating cache, unless the view# specifies explicitly it should not be added to breadcrumb or the# view is a binary viewifview.add_to_breadcrumbsandnotview.binary:self.req.update_breadcrumbs()defvalidate_cache(self,view):view.set_http_cache_headers()self.req.validate_cache()defexecute_linkto(self,eid=None):"""XXX __linkto parameter may cause security issue defined here since custom application controller inheriting from this one use this method? """req=self.reqifnot'__linkto'inreq.form:returnifeidisNone:eid=typed_eid(req.form['eid'])forlinktoinreq.list_form_param('__linkto',pop=True):rtype,eids,target=linkto.split(':')asserttargetin('subject','object')eids=eids.split('_')iftarget=='subject':rql='SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s'%rtypeelse:rql='SET Y %s X WHERE X eid %%(x)s, Y eid %%(y)s'%rtypeforteidineids:req.execute(rql,{'x':eid,'y':typed_eid(teid)},('x','y'))classFormValidatorController(Controller):id='validateform'defpublish(self,rset=None):vreg=self.vregtry:ctrl=vreg.select(vreg.registry_objects('controllers','edit'),req=self.req,appli=self.appli)exceptNoSelectableObject:status,args=(False,{None:self.req._('not authorized')})else:try:ctrl.publish(None,fromjson=True)exceptValidationError,err:status,args=self.validation_error(err)exceptRedirect,err:try:self.req.cnx.commit()# ValidationError may be raise on commitexceptValidationError,err:status,args=self.validation_error(err)else:status,args=(True,err.location)exceptException,err:self.req.cnx.rollback()self.exception('unexpected error in validateform')try:status,args=(False,self.req._(unicode(err)))exceptUnicodeError:status,args=(False,repr(err))else:status,args=(False,'???')self.req.set_content_type('text/html')jsarg=simplejson.dumps((status,args))return"""<script type="text/javascript"> window.parent.handleFormValidationResponse('entityForm', null, null, %s);</script>"""%simplejson.dumps((status,args))defvalidation_error(self,err):self.req.cnx.rollback()try:eid=err.entity.eidexceptAttributeError:eid=err.entityreturn(False,(eid,err.errors))classJSonController(Controller):id='json'defpublish(self,rset=None):"""call js_* methods. Expected form keys: :fname: the method name without the js_ prefix :args: arguments list (json) note: it's the responsability of js_* methods to set the correct response content type """self.req.pageid=self.req.form.get('pageid')fname=self.req.form['fname']try:func=getattr(self,'js_%s'%fname)exceptAttributeError:raiseRemoteCallFailed('no %s method'%fname)# no <arg> attribute means the callback takes no argumentargs=self.req.form.get('arg',())ifnotisinstance(args,(list,tuple)):args=(args,)args=[simplejson.loads(arg)forarginargs]try:result=func(*args)exceptRemoteCallFailed:raiseexceptException,ex:self.exception('an exception occured while calling js_%s(%s): %s',fname,args,ex)raiseRemoteCallFailed(repr(ex))ifresultisNone:return''# get unicode on @htmlize methods, encoded string on @jsonize methodselifisinstance(result,unicode):returnresult.encode(self.req.encoding)returnresultdef_rebuild_posted_form(self,names,values,action=None):form={}forname,valueinzip(names,values):# remove possible __action_xxx inputsifname.startswith('__action'):continue# form.setdefault(name, []).append(value)ifnameinform:curvalue=form[name]ifisinstance(curvalue,list):curvalue.append(value)else:form[name]=[curvalue,value]else:form[name]=value# simulate click on __action_%s button to help the controllerifaction:form['__action_%s'%action]=u'whatever'returnformdef_exec(self,rql,args=None,eidkey=None,rocheck=True):"""json mode: execute RQL and return resultset as json"""ifrocheck:self.ensure_ro_rql(rql)try:returnself.req.execute(rql,args,eidkey)exceptException,ex:self.exception("error in _exec(rql=%s): %s",rql,ex)returnNonereturnNone@xhtmlizedefjs_view(self):# XXX try to use the page-content templatereq=self.reqrql=req.form.get('rql')ifrql:rset=self._exec(rql)else:rset=Nonevid=req.form.get('vid')orvid_from_rset(req,rset,self.schema)try:view=self.vreg.select_view(vid,req,rset)exceptNoSelectableObject:vid=req.form.get('fallbackvid','noresult')view=self.vreg.select_view(vid,req,rset)divid=req.form.get('divid','pageContent')# we need to call pagination before with the stream setstream=view.set_stream()ifreq.form.get('paginate'):ifdivid=='pageContent':# mimick main template behaviourstream.write(u'<div id="pageContent">')vtitle=self.req.form.get('vtitle')ifvtitle:stream.write(u'<h1 class="vtitle">%s</h1>\n'%vtitle)view.pagination(req,rset,view.w,notview.need_navigation)ifdivid=='pageContent':stream.write(u'<div id="contentmain">')view.render()extresources=req.html_headers.getvalue(skiphead=True)ifextresources:stream.write(u'<div class="ajaxHtmlHead">\n')# XXX use a widget ?stream.write(extresources)stream.write(u'</div>\n')ifreq.form.get('paginate')anddivid=='pageContent':stream.write(u'</div></div>')returnstream.getvalue()@xhtmlizedefjs_prop_widget(self,propkey,varname,tabindex=None):"""specific method for CWProperty handling"""entity=self.vreg.etype_class('CWProperty')(self.req,None,None)entity.eid=varnameentity['pkey']=propkeyform=self.vreg.select_object('forms','edition',self.req,None,entity=entity)form.form_build_context()vfield=form.field_by_name('value')renderer=FormRenderer(self.req)returnvfield.render(form,renderer,tabindex=tabindex) \+renderer.render_help(form,vfield)@xhtmlizedefjs_component(self,compid,rql,registry='components',extraargs=None):ifrql:rset=self._exec(rql)else:rset=Nonecomp=self.vreg.select_object(registry,compid,self.req,rset)ifextraargsisNone:extraargs={}else:# we receive unicode keys which is not supported by the **syntaxextraargs=dict((str(key),value)forkey,valueinextraargs.items())extraargs=extraargsor{}returncomp.render(**extraargs)@check_pageid@xhtmlizedefjs_inline_creation_form(self,peid,ttype,rtype,role):view=self.vreg.select_view('inline-creation',self.req,None,etype=ttype,peid=peid,rtype=rtype,role=role)returnview.render(etype=ttype,peid=peid,rtype=rtype,role=role)@jsonizedefjs_validate_form(self,action,names,values):returnself.validate_form(action,names,values)defvalidate_form(self,action,names,values):# XXX this method (and correspoding js calls) should use the new# `RemoteCallFailed` mechansimself.req.form=self._rebuild_posted_form(names,values,action)vreg=self.vregtry:ctrl=vreg.select(vreg.registry_objects('controllers','edit'),req=self.req)exceptNoSelectableObject:return(False,{None:self.req._('not authorized')})try:ctrl.publish(None,fromjson=True)exceptValidationError,err:self.req.cnx.rollback()return(False,(err.entity,err.errors))exceptRedirect,redir:return(True,redir.location)exceptException,err:self.req.cnx.rollback()self.exception('unexpected error in js_validateform')return(False,self.req._(str(err).decode('utf-8')))return(False,'???')@jsonizedefjs_edit_field(self,action,names,values,rtype,eid,default):success,args=self.validate_form(action,names,values)ifsuccess:# Any X,N where we don't seem to use N is an optimisation# printable_value won't need to query N againrset=self.req.execute('Any X,N WHERE X eid %%(x)s, X %s N'%rtype,{'x':eid},'x')entity=rset.get_entity(0,0)value=entity.printable_value(rtype)return(success,args,valueordefault)else:return(success,args,None)@jsonizedefjs_edit_relation(self,action,names,values,rtype,role,eid,vid,default):success,args=self.validate_form(action,names,values)ifsuccess:entity=self.req.eid_rset(eid).get_entity(0,0)rset=entity.related(rtype,role)ifrset:output=self.view(vid,rset)ifvid=='textoutofcontext':output=html_escape(output)else:output=defaultreturn(success,args,output)else:return(success,args,None)@jsonizedefjs_i18n(self,msgids):"""returns the translation of `msgid`"""return[self.req._(msgid)formsgidinmsgids]@jsonizedefjs_format_date(self,strdate):"""returns the formatted date for `msgid`"""date=strptime(strdate,'%Y-%m-%d %H:%M:%S')returnself.format_date(date)@jsonizedefjs_external_resource(self,resource):"""returns the URL of the external resource named `resource`"""returnself.req.external_resource(resource)@check_pageid@jsonizedefjs_user_callback(self,cbname):page_data=self.req.get_session_data(self.req.pageid,{})try:cb=page_data[cbname]exceptKeyError:returnNonereturncb(self.req)ifHAS_SEARCH_RESTRICTION:@jsonizedefjs_filter_build_rql(self,names,values):form=self._rebuild_posted_form(names,values)self.req.form=formbuilder=FilterRQLBuilder(self.req)returnbuilder.build_rql()@jsonizedefjs_filter_select_content(self,facetids,rql):rqlst=self.vreg.parse(self.req,rql)# XXX Union unsupported yetmainvar=prepare_facets_rqlst(rqlst)[0]update_map={}forfacetidinfacetids:facet=get_facet(self.req,facetid,rqlst.children[0],mainvar)update_map[facetid]=facet.possible_values()returnupdate_mapdefjs_unregister_user_callback(self,cbname):self.req.unregister_callback(self.req.pageid,cbname)defjs_unload_page_data(self):self.req.del_session_data(self.req.pageid)defjs_cancel_edition(self,errorurl):"""cancelling edition from javascript We need to clear associated req's data : - errorurl - pending insertions / deletions """self.req.cancel_edition(errorurl)defjs_delete_bookmark(self,beid):rql='DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s'self.req.execute(rql,{'b':typed_eid(beid),'u':self.req.user.eid})defjs_node_clicked(self,treeid,nodeeid):"""add/remove eid in treestate cookie"""fromcubicweb.web.views.treeviewimporttreecookienamecookies=self.req.get_cookie()statename=treecookiename(treeid)treestate=cookies.get(statename)iftreestateisNone:cookies[statename]=nodeeidself.req.set_cookie(cookies,statename)else:marked=set(filter(None,treestate.value.split(';')))ifnodeeidinmarked:marked.remove(nodeeid)else:marked.add(nodeeid)cookies[statename]=';'.join(marked)self.req.set_cookie(cookies,statename)defjs_set_cookie(self,cookiename,cookievalue):# XXX we should consider jQuery.Cookiecookiename,cookievalue=str(cookiename),str(cookievalue)cookies=self.req.get_cookie()cookies[cookiename]=cookievalueself.req.set_cookie(cookies,cookiename)# relations edition stuff ##################################################def_add_pending(self,eidfrom,rel,eidto,kind):key='pending_%s'%kindpendings=self.req.get_session_data(key,set())pendings.add((typed_eid(eidfrom),rel,typed_eid(eidto)))self.req.set_session_data(key,pendings)def_remove_pending(self,eidfrom,rel,eidto,kind):key='pending_%s'%kindpendings=self.req.get_session_data(key)pendings.remove((typed_eid(eidfrom),rel,typed_eid(eidto)))self.req.set_session_data(key,pendings)defjs_remove_pending_insert(self,(eidfrom,rel,eidto)):self._remove_pending(eidfrom,rel,eidto,'insert')defjs_add_pending_inserts(self,tripletlist):foreidfrom,rel,eidtointripletlist:self._add_pending(eidfrom,rel,eidto,'insert')defjs_remove_pending_delete(self,(eidfrom,rel,eidto)):self._remove_pending(eidfrom,rel,eidto,'delete')defjs_add_pending_delete(self,(eidfrom,rel,eidto)):self._add_pending(eidfrom,rel,eidto,'delete')# XXX specific code. Kill me and my AddComboBox friend@jsonizedefjs_add_and_link_new_entity(self,etype_to,rel,eid_to,etype_from,value_from):# create a new entityeid_from=self.req.execute('INSERT %s T : T name "%s"'%(etype_from,value_from))[0][0]# link the new entity to the main entityrql='SET F %(rel)s T WHERE F eid %(eid_to)s, T eid %(eid_from)s'%{'rel':rel,'eid_to':eid_to,'eid_from':eid_from}returneid_fromclassSendMailController(Controller):id='sendmail'__select__=match_user_groups('managers','users')defrecipients(self):"""returns an iterator on email's recipients as entities"""eids=self.req.form['recipient']# make sure we have a list even though only one recipient was specifiedifisinstance(eids,basestring):eids=(eids,)rql='Any X WHERE X eid in (%s)'%(','.join(eids))rset=self.req.execute(rql)forentityinrset.entities():entity.complete()# XXX really?yieldentity@property@cacheddefsmtp(self):mailhost,port=self.config['smtp-host'],self.config['smtp-port']try:returnSMTP(mailhost,port)exceptException,ex:self.exception("can't connect to smtp server %s:%s (%s)",mailhost,port,ex)url=self.build_url(__message=self.req._('could not connect to the SMTP server'))raiseRedirect(url)defsendmail(self,recipient,subject,body):helo_addr='%s <%s>'%(self.config['sender-name'],self.config['sender-addr'])msg=format_mail({'email':self.req.user.get_email(),'name':self.req.user.dc_title(),},[recipient],body,subject)self.smtp.sendmail(helo_addr,[recipient],msg.as_string())defpublish(self,rset=None):# XXX this allow anybody with access to an cubicweb application to use it as a mail relaybody=self.req.form['mailbody']subject=self.req.form['subject']forrecipientinself.recipients():text=body%recipient.as_email_context()self.sendmail(recipient.get_email(),subject,text)# breadcrumbs = self.req.get_session_data('breadcrumbs', None)url=self.build_url(__message=self.req._('emails successfully sent'))raiseRedirect(url)classMailBugReportController(SendMailController):id='reportbug'__select__=yes()defpublish(self,rset=None):body=self.req.form['description']self.sendmail(self.config['submit-mail'],_('%s error report')%self.config.appid,body)url=self.build_url(__message=self.req._('bug report sent'))raiseRedirect(url)