# -*- coding: utf-8 -*-"""Set of base controllers, which are directly plugged into the applicationobject to handle publication.:organization: Logilab:copyright: 2001-2010 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.common.dateimportstrptimefromcubicwebimport(NoSelectableObject,ValidationError,ObjectNotFound,typed_eid)fromcubicweb.utilsimportCubicWebJsonEncoderfromcubicweb.selectorsimportyes,match_user_groupsfromcubicweb.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=Falsedefjsonize(func):"""decorator to sets correct content_type and calls `simplejson.dumps` on results """defwrapper(self,*args,**kwargs):self._cw.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._cw.set_content_type(self._cw.html_content_type())result=func(self,*args,**kwargs)return''.join((self._cw.document_surrounding_div(),result.strip(),u'</div>'))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._cw.get_session_data(self._cw.pageid)ifdataisNone:raiseRemoteCallFailed(self._cw._('pageid-not-found'))returnfunc(self,*args,**kwargs)returnwrapperclassLoginController(Controller):__regid__='login'defpublish(self,rset=None):"""log in the instance"""ifself._cw.vreg.config['auth-mode']=='http':# HTTP authenticationraiseExplicitLogin()else:# Cookie authenticationreturnself.appli.need_login_content(self._cw)classLogoutController(Controller):__regid__='logout'defpublish(self,rset=None):"""logout from the instance"""returnself.appli.session_handler.logout(self._cw,self.goto_url())defgoto_url(self):# * in http auth mode, url will be ignored# * in cookie mode redirecting to the index view is enough : either# anonymous connection is allowed and the page will be displayed or# we'll be redirected to the login formmsg=self._cw._('you have been logged out')ifself._cw.https:# XXX hack to generate an url on the http version of the siteself._cw._base_url=self._cw.vreg.config['base-url']self._cw.https=Falsereturnself._cw.build_url('view',vid='index',__message=msg)classViewController(Controller):"""standard entry point : - build result set - select and call main template """__regid__='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._cw)returnself._cw.vreg['views'].main_template(self._cw,template,rset=rset,view=view)def_select_view_and_rset(self,rset):req=self._cwifrsetisNoneandnothasattr(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._cw.vreg.schema)try:view=self._cw.vreg['views'].select(vid,req,rset=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._cw.vreg.schema)view=self._cw.vreg['views'].select(vid,req,rset=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 can not ""be used to display the current data."))self.warning("the view %s can not be applied to this query",vid)vid=req.form.get('fallbackvid')orvid_from_rset(req,rset,req.vreg.schema)view=req.vreg['views'].select(vid,req,rset=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._cw.update_breadcrumbs()defvalidate_cache(self,view):view.set_http_cache_headers()self._cw.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._cwifnot'__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'))def_validation_error(req,ex):req.cnx.rollback()# XXX necessary to remove existant validation error?# imo (syt), it's not necessaryreq.get_session_data(req.form.get('__errorurl'),pop=True)foreid=ex.entityeidmap=req.data.get('eidmap',{})forvar,eidineidmap.items():ifforeid==eid:foreid=varbreakreturn(foreid,ex.errors)def_validate_form(req,vreg):# XXX should use the `RemoteCallFailed` mechanismtry:ctrl=vreg['controllers'].select('edit',req=req)exceptNoSelectableObject:return(False,{None:req._('not authorized')},None)try:ctrl.publish(None)exceptValidationError,ex:return(False,_validation_error(req,ex),ctrl._edited_entity)exceptRedirect,ex:try:req.cnx.commit()# ValidationError may be raise on commitexceptValidationError,ex:return(False,_validation_error(req,ex),ctrl._edited_entity)exceptException,ex:req.cnx.rollback()req.exception('unexpected error while validating form')return(False,req._(str(ex).decode('utf-8')),ctrl._edited_entity)else:# complete entity: it can be used in js callbacks where we might# want every possible informationifctrl._edited_entity:ctrl._edited_entity.complete()return(True,ex.location,ctrl._edited_entity)exceptException,ex:req.cnx.rollback()req.exception('unexpected error while validating form')return(False,req._(str(ex).decode('utf-8')),ctrl._edited_entity)return(False,'???',None)classFormValidatorController(Controller):__regid__='validateform'defresponse(self,domid,status,args,entity):callback=str(self._cw.form.get('__onsuccess','null'))errback=str(self._cw.form.get('__onfailure','null'))cbargs=str(self._cw.form.get('__cbargs','null'))self._cw.set_content_type('text/html')jsargs=simplejson.dumps((status,args,entity),cls=CubicWebJsonEncoder)return"""<script type="text/javascript"> wp = window.parent; window.parent.handleFormValidationResponse('%s', %s, %s, %s, %s);</script>"""%(domid,callback,errback,jsargs,cbargs)defpublish(self,rset=None):self._cw.json_request=True# XXX unclear why we have a separated controller here vs# js_validate_form on the json controllerstatus,args,entity=_validate_form(self._cw,self._cw.vreg)domid=self._cw.form.get('__domid','entityForm').encode(self._cw.encoding)returnself.response(domid,status,args,entity)classJSonController(Controller):__regid__='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._cw.json_request=Truetry:fname=self._cw.form['fname']func=getattr(self,'js_%s'%fname)exceptKeyError:raiseRemoteCallFailed('no method specified')exceptAttributeError:raiseRemoteCallFailed('no %s method'%fname)# no <arg> attribute means the callback takes no argumentargs=self._cw.form.get('arg',())ifnotisinstance(args,(list,tuple)):args=(args,)args=[simplejson.loads(arg)forarginargs]try:result=func(*args)exceptRemoteCallFailed:raiseexceptException,ex:importtracebacktraceback.print_exc()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._cw.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._cw.ensure_ro_rql(rql)try:returnself._cw.execute(rql,args,eidkey)exceptException,ex:self.exception("error in _exec(rql=%s): %s",rql,ex)returnNonereturnNonedef_call_view(self,view,**kwargs):req=self._cwdivid=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._cw.form.get('vtitle')ifvtitle:stream.write(u'<h1 class="vtitle">%s</h1>\n'%vtitle)view.paginate()ifdivid=='pageContent':stream.write(u'<div id="contentmain">')view.render(**kwargs)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_view(self):# XXX try to use the page-content templatereq=self._cwrql=req.form.get('rql')ifrql:rset=self._exec(rql)else:rset=Nonevid=req.form.get('vid')orvid_from_rset(req,rset,self._cw.vreg.schema)try:view=self._cw.vreg['views'].select(vid,req,rset=rset)exceptNoSelectableObject:vid=req.form.get('fallbackvid','noresult')view=self._cw.vreg['views'].select(vid,req,rset=rset)self.validate_cache(view)returnself._call_view(view)@xhtmlizedefjs_prop_widget(self,propkey,varname,tabindex=None):"""specific method for CWProperty handling"""entity=self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)entity.eid=varnameentity['pkey']=propkeyform=self._cw.vreg['forms'].select('edition',self._cw,entity=entity)form.build_context()vfield=form.field_by_name('value')renderer=FormRenderer(self._cw)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=NoneifextraargsisNone:extraargs={}else:# we receive unicode keys which is not supported by the **syntaxextraargs=dict((str(key),value)forkey,valueinextraargs.items())comp=self._cw.vreg[registry].select(compid,self._cw,rset=rset,**extraargs)extraargs=extraargsor{}returncomp.render(**extraargs)@check_pageid@xhtmlizedefjs_inline_creation_form(self,peid,petype,ttype,rtype,role,i18nctx):view=self._cw.vreg['views'].select('inline-creation',self._cw,etype=ttype,rtype=rtype,role=role,peid=peid,petype=petype)returnself._call_view(view,i18nctx=i18nctx)@jsonizedefjs_validate_form(self,action,names,values):returnself.validate_form(action,names,values)defvalidate_form(self,action,names,values):self._cw.form=self._rebuild_posted_form(names,values,action)return_validate_form(self._cw,self._cw.vreg)@xhtmlizedefjs_reledit_form(self):req=self._cwargs=dict((x,self._cw.form[x])forxinfrozenset(('rtype','role','reload','landing_zone')))entity=self._cw.entity_from_eid(int(self._cw.form['eid']))# note: default is reserved in js landargs['default']=self._cw.form['default_value']args['reload']=simplejson.loads(args['reload'])rset=req.eid_rset(int(self._cw.form['eid']))view=req.vreg['views'].select('doreledit',req,rset=rset,rtype=args['rtype'])stream=view.set_stream()view.render(**args)extresources=req.html_headers.getvalue(skiphead=True)ifextresources:stream.write(u'<div class="ajaxHtmlHead">\n')stream.write(extresources)stream.write(u'</div>\n')returnstream.getvalue()@jsonizedefjs_i18n(self,msgids):"""returns the translation of `msgid`"""return[self._cw._(msgid)formsgidinmsgids]@jsonizedefjs_format_date(self,strdate):"""returns the formatted date for `msgid`"""date=strptime(strdate,'%Y-%m-%d %H:%M:%S')returnself._cw.format_date(date)@jsonizedefjs_external_resource(self,resource):"""returns the URL of the external resource named `resource`"""returnself._cw.external_resource(resource)@check_pageid@jsonizedefjs_user_callback(self,cbname):page_data=self._cw.get_session_data(self._cw.pageid,{})try:cb=page_data[cbname]exceptKeyError:returnNonereturncb(self._cw)ifHAS_SEARCH_RESTRICTION:@jsonizedefjs_filter_build_rql(self,names,values):form=self._rebuild_posted_form(names,values)self._cw.form=formbuilder=FilterRQLBuilder(self._cw)returnbuilder.build_rql()@jsonizedefjs_filter_select_content(self,facetids,rql):rqlst=self._cw.vreg.parse(self._cw,rql)# XXX Union unsupported yetmainvar=prepare_facets_rqlst(rqlst)[0]update_map={}forfacetidinfacetids:facet=get_facet(self._cw,facetid,rqlst.children[0],mainvar)update_map[facetid]=facet.possible_values()returnupdate_mapdefjs_unregister_user_callback(self,cbname):self._cw.unregister_callback(self._cw.pageid,cbname)defjs_unload_page_data(self):self._cw.del_session_data(self._cw.pageid)defjs_cancel_edition(self,errorurl):"""cancelling edition from javascript We need to clear associated req's data : - errorurl - pending insertions / deletions """self._cw.cancel_edition(errorurl)defjs_delete_bookmark(self,beid):rql='DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s'self._cw.execute(rql,{'b':typed_eid(beid),'u':self._cw.user.eid})defjs_node_clicked(self,treeid,nodeeid):"""add/remove eid in treestate cookie"""fromcubicweb.web.views.treeviewimporttreecookienamecookies=self._cw.get_cookie()statename=treecookiename(treeid)treestate=cookies.get(statename)iftreestateisNone:cookies[statename]=nodeeidself._cw.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._cw.set_cookie(cookies,statename)@jsonizedefjs_set_cookie(self,cookiename,cookievalue):# XXX we should consider jQuery.Cookiecookiename,cookievalue=str(cookiename),str(cookievalue)cookies=self._cw.get_cookie()cookies[cookiename]=cookievalueself._cw.set_cookie(cookies,cookiename)# relations edition stuff ##################################################def_add_pending(self,eidfrom,rel,eidto,kind):key='pending_%s'%kindpendings=self._cw.get_session_data(key,set())pendings.add((typed_eid(eidfrom),rel,typed_eid(eidto)))self._cw.set_session_data(key,pendings)def_remove_pending(self,eidfrom,rel,eidto,kind):key='pending_%s'%kindpendings=self._cw.get_session_data(key)pendings.remove((typed_eid(eidfrom),rel,typed_eid(eidto)))self._cw.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._cw.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):__regid__='sendmail'__select__=match_user_groups('managers','users')defrecipients(self):"""returns an iterator on email's recipients as entities"""eids=self._cw.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._cw.execute(rql)forentityinrset.entities():yieldentity@property@cacheddefsmtp(self):mailhost,port=self._cw.config['smtp-host'],self._cw.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._cw.build_url(__message=self._cw._('could not connect to the SMTP server'))raiseRedirect(url)defsendmail(self,recipient,subject,body):helo_addr='%s <%s>'%(self._cw.config['sender-name'],self._cw.config['sender-addr'])msg=format_mail({'email':self._cw.user.get_email(),'name':self._cw.user.dc_title(),},[recipient],body,subject)self.smtp.sendmail(helo_addr,[recipient],msg.as_string())defpublish(self,rset=None):# XXX this allows users with access to an cubicweb instance to use it as# a mail relaybody=self._cw.form['mailbody']subject=self._cw.form['subject']forrecipientinself.recipients():text=body%recipient.as_email_context()self.sendmail(recipient.get_email(),subject,text)# breadcrumbs = self._cw.get_session_data('breadcrumbs', None)url=self._cw.build_url(__message=self._cw._('emails successfully sent'))raiseRedirect(url)classMailBugReportController(SendMailController):__regid__='reportbug'__select__=yes()defpublish(self,rset=None):body=self._cw.form['description']self.sendmail(self._cw.config['submit-mail'],_('%s error report')%self._cw.config.appid,body)url=self._cw.build_url(__message=self._cw._('bug report sent'))raiseRedirect(url)