1 # this is the abstract interface for Plugin instances
2 # so it should be specialized in real plugin classes
3 # like e.g. plugins.simplelist.SimpleList
5 from __future__ import print_function
9 from django.template.loader import render_to_string
11 from unfold.page import Page
12 from unfold.prelude import Prelude
17 # . [ 'SliceList', 'TabbedView' ] : to debug these classes
18 # . True : to debug all plugin
21 #DEBUG= [ 'SimpleList' ]
24 # decorator to deflect calls on Plugin to its Prelude through self.page.prelude
25 def to_prelude (method):
26 def actual (self, *args, **kwds):
27 if not self.page: # jordan
29 prelude_method=Prelude.__dict__[method.__name__]
30 return prelude_method(self.page.prelude,*args, **kwds)
35 # using a simple incremental scheme to generate domids for now
36 # we just need this to be unique in a page
39 # when a domid is not set by the caller, we name plugins after their respective class as well,
40 # so as to limit name clashes between different views
41 # this has to see with the UI storing the last-seen status of plugins based on their id
42 # put it more bluntly it is recommended that a domid should be set
43 # and maybe xxx we should just enforce that...
46 return "plugin-%s-%d"%(self.__class__.__name__.lower(),Plugin.domid)
51 # . page: the context of the request being served
52 # . title: is used visually for displaying the widget
54 # . visible: if not set the plugin does not show up at all
55 # (not quite sure what this was for)
56 # . togglable: whether it can be turned on and off by clicking on the title (like PleKitToggle)
57 # . toggled: whether the plugin should startup open/shown or closed/hidden
59 # .. True : start up open/hidden
60 # .. False : start up closed/shown
61 # .. 'persistent' : start up as it was the last time that browser showed it (based on 'domid')
62 # NOTE that it is required for you to set a domid if you want to use persistent mode
63 # since domid is the key for storing that data in the browser storage space
64 # .. None : if not passed to __init__ at all, then the default_toggled() method is called
65 # .. : anything else, defaults to True
66 # . outline_complete: whether the overall plugin (body + toggle buttons/title) needs to receive
67 # a border and extra space
68 # . outline_body : same but for the plugin body only
69 # for these 2 outline_ flags, possible values mimick the above behaviour, i.e.:
70 # .. True: : outline is on
71 # .. False: : outline is off
72 # .. None: : calls default_outline_complete() on the plugin object
75 # . domid: created internally, but can be set at creation time if needed
76 # useful for hand-made css, or for selecting an active plugin in a composite
77 # . rank: this is for plugins sons of a composite plugin
79 # any other setting can also be set when creating the object, like
81 # which will result in 'foo' being accessible to the template engine
83 def __init__ (self, page, title=None, domid=None,
84 visible=True, togglable=None, toggled=None,
85 outline_complete=None, outline_body=None,
88 # callers can provide their domid for css'ing
89 if not domid: domid=self.newdomid()
91 # title is shown when togglable
92 #if not title: title="Plugin title for %s"%domid
94 self.classname=self._py_classname()
95 self.plugin_classname=self._js_classname()
97 if togglable is None: self.togglable=self.default_togglable()
98 else: self.togglable=togglable
99 if toggled is None: self.toggled=self.default_toggled()
100 else: self.toggled=toggled
101 if outline_complete is None: self.outline_complete=self.default_outline_complete()
102 else: self.outline_complete=outline_complete
103 if outline_body is None: self.outline_body=self.default_outline_body()
104 else: self.outline_body=outline_body
105 # what comes from subclasses
106 for (k,v) in settings.iteritems():
108 if self.need_debug(): print("%s init - subclass setting %s"%(self.classname,k))
110 if self.need_debug():
111 print("%s init dbg .... BEG"%self.classname)
112 for (k,v) in self.__dict__.items(): print("dbg %s:%s"%(k,v))
113 print("%s init dbg .... END"%self.classname)
114 # do this only once the structure is fine
115 if self.page: # I assume we can have a None page (Jordan)
116 self.page.record_plugin(self)
119 return "[%s]:%s"%(self.classname,self.domid)
121 def _py_classname (self):
122 try: return self.__class__.__name__
123 except: return 'Plugin'
125 def _js_classname (self):
126 try: return self.plugin_classname ()
127 except: return self._py_classname()
130 def need_debug (self):
131 if not DEBUG: return False
132 if DEBUG is True: return True
133 else: return self.classname in DEBUG
135 def setting_json (self, setting):
136 # TMP: js world expects plugin_uuid
137 if setting=='plugin_uuid':
139 elif setting=='query_uuid':
140 try: value=self.query.query_uuid
141 except: return '%s:"undefined"'%setting
143 value=getattr(self,setting,None)
144 if value is None: value = "unknown-setting-%s"%setting
145 # first try to use to_json method (json.dumps not working on class instances)
146 try: value_json=value.to_json()
147 except: value_json=json.dumps(value,separators=(',',':'))
148 return "%s:%s"%(setting,value_json)
150 # expose in json format to js the list of fields as described in json_settings_list()
151 # and add plugin_uuid: domid in the mix
152 # NOTE this plugin_uuid thing might occur in js files from joomla/js, ** do not rename **
153 def settings_json (self):
154 exposed_settings=self.json_settings_list()
155 if 'query' in exposed_settings:
156 print("WARNING, cannot expose 'query' directly in json_settings_list, query_uuid is enough")
158 result += ",".join([ self.setting_json(setting) for setting in self.json_settings_list() ])
162 # as a first approximation, only plugins that are associated with a query
163 # need to be prepared for js - meaning their json settings get exposed to js
164 # others just get displayed and that's it
165 def export_json_settings (self):
166 return 'query_uuid' in self.json_settings_list()
168 # returns the html code for that plugin
169 # in essence, wraps the results of self.render_content ()
170 def render (self, request):
171 # call render_content
172 plugin_content = self.render_content (request)
173 # shove this into plugin.html
175 env ['plugin_content']= plugin_content
176 env.update(self.__dict__)
177 # translate high-level 'toggled' into 4 different booleans
178 self.need_toggle = False
179 if self.toggled=='persistent':
180 # start with everything turned off and let the js callback do its job
181 env.update({'persistent_toggle':True,'display_hide_button':False,
182 'display_show_button':False,'display_body':False})
183 elif self.toggled==False:
184 env.update({'persistent_toggle':False,'display_hide_button':False,
185 'display_show_button':True,'display_body':False})
187 env.update({'persistent_toggle':False,'display_hide_button':True,
188 'display_show_button':False,'display_body':True})
189 if self.need_debug():
190 print("rendering plugin.html with env keys %s"%env.keys())
191 for (k,v) in env.items():
192 if "display" in k or "persistent" in k: print(k,'->',v)
193 result = render_to_string ('plugin.html',env)
195 # export this only for relevant plugins
196 if self.export_json_settings():
197 env ['settings_json' ] = self.settings_json()
198 # compute plugin-specific initialization
199 js_init = render_to_string ( 'plugin-init.js', env )
200 # make sure this happens first in js
201 self.add_js_init_chunks (js_init)
203 # interpret the result of requirements ()
204 self.handle_requirements (request)
208 # you may redefine this completely, but if you don't we'll just use methods
209 # . template_file() to find out which template to use, and
210 # . template_env() to compute a dictionary to pass along to the templating system
211 def render_content (self, request):
212 """Should return an HTML fragment"""
213 template = self.template_file()
214 # start with a fresh one
216 # add our own settings as defaults
217 env.update(self.__dict__)
218 # then the things explicitly defined in template_env()
219 env.update(self.template_env(request))
220 if not isinstance (env,dict):
221 raise Exception, "%s.template_env returns wrong type"%self.classname
222 result=render_to_string (template, env)
223 if self.need_debug():
224 print("%s.render_content: BEG --------------------"%self.classname)
225 print("template=%s"%template)
226 print("env.keys=%s"%env.keys())
229 print("%s.render_content: END --------------------"%self.classname)
232 # or from the result of self.requirements()
233 def handle_requirements (self, request):
235 d=self.requirements()
236 for (k,v) in d.iteritems():
237 if self.need_debug():
238 print("%s: handling requirement %s"%(self.classname,v))
239 # e.g. js_files -> add_js_files
241 method=Page.__dict__[method_name]
243 except AttributeError:
244 # most likely the object does not have that method defined, which is fine
248 traceback.print_exc()
251 #################### requirements/prelude management
252 # just forward to our prelude instance - see decorator above
254 def add_js_files (self):pass
256 def add_css_files (self):pass
258 def add_js_init_chunks (self):pass
260 def add_js_chunks (self):pass
262 def add_css_chunks (self):pass
264 ######################################## abstract interface
265 # your plugin is expected to implement either
266 # (*) def render_content(self, request) -> html fragment
268 # (*) def template_file (self) -> filename
270 # (*) def template_env (self, request) -> dict
271 # this is the variable->value association used to render the template
272 # in which case the html template will be used
274 # if you see this string somewhere your template_file() code is not kicking in
275 def template_file (self): return "undefined_template"
276 def template_env (self, request): return {}
278 def default_togglable (self): return False
279 def default_toggled (self): return 'persistent'
280 def default_outline_complete (self):return False
281 def default_outline_body(self): return False
283 # # tell the framework about requirements (for the document <header>)
284 # # the notion of 'Media' in django provides for medium-dependant
285 # # selection of css files
286 # # as a first attempt however we keep a flat model for now
287 # # can use one string instead of a list or tuple if needed,
288 # # see requirements.py for details
289 # def requirements (self):
290 # return { 'js_files' : [], # a list of relative paths for js input files
291 # 'css_files': [], # ditto for css, could have been a dict keyed on
293 # 'js_chunk' : [], # (lines of) verbatim javascript code
294 # 'css_chunk': [], # likewise for css scripts
297 # # for better performance
298 # # you can specify a list of keys that won't be exposed as json attributes
299 # def exclude_from_json (self): return []
301 # mandatory : define the fields that need to be exposed to json as part of
302 # plugin initialization
303 # mention 'domid' if you need plugin_uuid
304 # also 'query_uuid' gets replaced with query.query_uuid
305 def json_settings_list (self): return ['json_settings_list-must-be-redefined']
307 # might also define these ones:
309 # see e.g. slicelist.py that piggybacks simplelist js code
310 # def plugin_classname (self)
312 # whether we export the json settings to js
313 # def export_json_settings (self)