3 from __future__
import with_statement
4 import re
, datetime
, time
, os
, sys
, gzip
, math
, compress
, hashlib
, httplib2
5 from cache
import Cache
6 from template
import Template
9 from session
import User
10 from settings
import Settings
11 from model
import LimoModel
12 from theme
import Theme
13 from limoutil
import *
15 class LimoBlock(Loggable
):
16 """ LimoBlock is the very basic block object, it represents some block on content, and its associated actions.
17 Most people will want to inherit from HTMLBlock, which adds several html specific capabilities.
19 def __init__(self
, request
, response
, session
):
20 self
.id = self
.__class
__.__name
__.lower().replace('block','')
21 self
.request
= request
22 self
.response
= response
23 self
.session
= session
24 self
.url
= [u
for u
in request
["REQUEST_URI"].split("/") if u
is not '']
26 # build self.args from both the query_string and the post_data
27 for p
in request
["QUERY_STRING"].split("&"):
30 self
.args
[t
[0]] = t
[1]
33 for p
in request
["POST_DATA"].split("&"):
36 self
.args
[t
[0]] = t
[1]
41 this is not __init__()
42 its not for object creation, its used as phase-1 of the rendering process
43 do things like requireCSS, requireJS, set headers, cookies, etc. here
46 def render(self
, next
=None):
47 """ render() is a generator function that yields some block of content.
48 If this Block is rendering as part of a pipeline of blocks,
49 'next' will be a generator for getting the rest of the stuff from the pipe
53 """ Sub-classes that define action() are expected to raise exceptions.
54 HTTPRedirect, and HTTPStatusError exceptions will be written back to the browser,
55 any other exception is considered an action failure and the exception is propagated.
60 class HTMLBlock(LimoBlock
):
61 def __init__(self
, request
, response
, session
):
62 LimoBlock
.__init
__(self
, request
, response
, session
)
69 self
.requireCSS("main.css")
71 def render(self
, next
=None):
72 """ render() is a generator function that yields some block of content.
73 If this Block is rendering as part of a pipeline of blocks,
74 'next' will be a generator for getting all the stuff rendered before us in the pipe
76 if self
.body
is not None:
77 yield self
.template("block", {'id': self
.id, 'body': self
.body
, 'title': self
.title
, 'after': self
.after
, 'before': self
.before
})
80 def getFile(self
, file,_theme
=None):
82 _theme
= self
.session
.data
.get('theme','default')
83 return Theme().getFile(file, _theme
)
85 def setPageTitle(self
, title
, append
=True):
86 """ Set some text for the page title. """
87 HeadBlock
.setTitle(title
,append
)
89 def _getRemoteFile(self
, file):
90 """ file is a remote url, which will be fetched with httplib2, written to disk, and its file name returned. """
91 fname
= file.split("?")[0]
92 fname
= re
.sub("[^A-Za-z0-9_]","_", fname
)
93 fname
= os
.path
.sep
.join((Settings
.cacheDir
,"_".join((fname
, hashlib
.sha1(file).hexdigest()))))
94 if not os
.path
.isfile(fname
):
95 self
.log("Downloading %s into %s" % (file,fname
))
96 (response
, content
) = httplib2
.Http().request(file)
97 with
open(fname
, "wb") as out
:
101 def requireCSS(self
, file):
102 """ Add a CSS file to the aggreggator for this page. """
103 if file[:6] in ("http:/","https:"):
104 fname
= self
._getRemoteFile
(file)
105 Cache
.CSS
.defaultFile(fname
)
107 Cache
.CSS
.defaultFile(self
.getFile(("css",file,)))
109 def requireJS(self
, file):
110 """ Add a JS file to the aggreggator for this page. """
111 if file[:6] in ("http:/","https:"):
112 fname
= self
._getRemoteFile
(file)
113 Cache
.JS
.defaultFile(fname
)
115 Cache
.JS
.defaultFile(self
.getFile(("js",file,)))
117 def template(self
, name
, params
):
118 """ Render the named template using the specified params.
120 if not name
.endswith(".tpl"):
122 yield Template
.render(self
.getFile(("templates",name
,)), params
)
124 def meta(self
, **kw
):
125 """ Add a <meta> tag to the <head>.
126 All keyword arguments become attributes.
128 HeadBlock
.addMeta(**kw
)
130 def link(self
, **kw
):
131 """ Add a <link> tag to the <head>.
132 All keyword arguments become attributes.
134 HeadBlock
.addLink(**kw
)
136 class TestBlockA(LimoBlock
):
137 def render(self
, next
=None):
139 yield LimoBlock
.render(self
, next
)
140 class TestBlockB(LimoBlock
):
141 def render(self
, next
=None):
143 yield LimoBlock
.render(self
, next
)
144 class TestBlockC(HTMLBlock
):
145 def render(self
, next
=None):
146 self
.title
= "TestBlock C"
148 self
.after
= "after C"
149 yield HTMLBlock
.render(self
, next
)
151 class EmptyBlock(LimoBlock
):
152 def render(self
, next
=None):
155 class SessionBlock(HTMLBlock
):
157 # self.log("args = %s" % self.args)
158 if self
.args
.get('action', False) == 'login':
159 u
= User
.getUser(self
.args
.get('username',None), self
.args
.get('password',None))
160 assert u
, "Login failed: No such username/password"
161 self
.session
.user
= u
162 assert self
.session
.hasRole('login'), "Account does not have login permission"
163 self
.session
.uid
= u
.uid
164 self
.session
.addMessage("%s, Thank you for logging in!" % u
.name
)
165 elif self
.args
.get('action', False) == 'edit':
166 uid
= int(self
.args
.get('uid', 0))
167 assert uid
> 0, "No user"
168 assert (self
.session
.uid
== uid
or self
.session
.hasRole('admin')), "Forbidden"
170 if self
.args
.get('newpass', False):
171 user
.setPassword(self
.args
['newpass'])
172 self
.session
.addMessage("Password updated.")
173 elif self
.args
.get('action', False) == 'logout':
174 if self
.session
.user
is not None:
176 self
.session
.user
= None
177 self
.session
.addMessage("Goodbye!")
178 def render(self
, next
=None):
179 if self
.session
.user
is None:
180 args
= { 'username': '' }
181 args
.update(self
.args
)
182 self
.title
= self
.template("title-login", args
)
183 self
.body
= self
.template("form-login", args
)
185 self
.title
= self
.template("title-welcome", {'name': self
.session
.user
.name
})
186 self
.body
= self
.template("body-welcome", {'name': self
.session
.user
.name
})
187 yield HTMLBlock
.render(self
, next
)
189 class VisitBlock(HTMLBlock
):
190 def render(self
, next
=None):
191 page
= int(self
.args
.get('p',1))
192 perpage
= int(self
.args
.get('pp',10))
193 rows
= Query("select * from visits order by vid desc limit %d, %d" %((page
-1)*perpage
, perpage
))
194 pages
= Query("select count(*) from visits")[0][0]
195 pages
= int(pages
/perpage
) + 1
196 self
.title
= "Visits"
197 self
.body
= self
.template("block-visits", locals())
198 return HTMLBlock
.render(self
, next
)
200 class DebugBlock(HTMLBlock
):
202 self
.requireJS("jquery.js")
203 self
.requireJS("debug.js")
204 self
.requireCSS("debug.css")
205 def render(self
, next
=None):
206 if self
.session
.hasRole('admin'):
207 self
.title
= "Debug Block"
208 queries
= [(sql
,ms
) for (sql
,ms
) in Query
.getQueryLog()]
209 Query
.clearQueryLog()
210 self
.body
= self
.template("body-debug", \
211 { 'counts': Template
.debug_counts
, \
212 'timings': Template
.debug_timing
, \
213 'queries': queries
, \
214 'request': self
.request
, \
215 'response': self
.response
, \
216 'session': htmlescape(str(self
.session
)), \
218 # HACK: TODO replace this
219 Template
.debug_timing
.clear()
220 Template
.debug_counts
.clear()
221 yield HTMLBlock
.render(self
, next
)
223 class HeadBlock(HTMLBlock
):
228 HeadBlock
.tags
.append( ('meta', kw
) )
231 HeadBlock
.tags
.append( ('link', kw
) )
233 def setTitle( text
, append
=True ):
235 HeadBlock
.title
+= text
237 HeadBlock
.title
= text
243 def render(self
, next
=None):
244 if self
.request
.get('HTTPS','') == 'on':
245 # dont bother downloading css or js over ssl? probably should have the option... to avoid a browser warning
247 js_file
= "http://"+self
.request
.get('HTTP_HOST','server')+"/"+Cache
.JS
.getOutputFile().replace(os
.path
.sep
,"/")
248 css_file
= "http://"+self
.request
.get('HTTP_HOST','server')+"/"+Cache
.CSS
.getOutputFile().replace(os
.path
.sep
,"/")
250 js_file
= "/"+Cache
.JS
.getOutputFile().replace(os
.path
.sep
,"/")
251 css_file
= "/"+Cache
.CSS
.getOutputFile().replace(os
.path
.sep
,"/")
253 yield self
.template("block-head", {
254 'title': HeadBlock
.title
,
257 'tags': ( ['<',tag
[0]] + [' %s="%s"' % (k
,v
) for k
,v
in tag
[1].items()] + [' />'] \
258 for tag
in HeadBlock
.tags
),
261 class StaticBlock(LimoBlock
):
262 """ Serves static files.
263 Does file-cached compression along the way.
265 # date format: Thu, 01 Dec 1994 16:00:00 GMT
266 http_date_format
= "%a, %d %b %Y %H:%M:%S"
267 http_date_format_tz
= "%a, %d %b %Y %H:%M:%S %Z"
268 def __init__(self
, request
, response
, session
):
269 LimoBlock
.__init
__(self
, request
, response
, session
)
275 'jpeg': 'image/jpeg',
278 'js': 'text/javascript',
283 def _setContentType(self
, file):
284 ext
= file.split(".")
285 if ext
[-1] in ("gzip","deflate","gz"):
290 type = self
.typemap
[ext
]
291 # set the content-type
292 # self.log("Setting Content-Type: %s" % type)
293 self
.response
.headers
['Content-Type'] = type
295 raise HTTPStatusError(404, "Cant determine content-type for file: %s" % file)
297 def _setExpires(self
, file):
298 # set an expires date one year in the future
299 self
.response
.headers
['Expires'] = (datetime
.datetime
.utcnow() + datetime
.timedelta(365)).strftime(self
.http_date_format_tz
)
301 def _setLastModified(self
, file):
302 # set the last-modified time
304 mtime
= datetime
.datetime
.fromtimestamp(os
.path
.getmtime(file))
305 mtime
= mtime
.replace(microsecond
=0)
306 # convert the mtime to utc
307 mtime
= mtime
- datetime
.timedelta(seconds
=time
.timezone
)
308 # self.log("Checking mtime of file: %s %s" % (file, mtime))
309 if self
.request
.get('HTTP_IF_MODIFIED_SINCE',False):
311 since
= datetime
.datetime
.strptime(self
.request
['HTTP_IF_MODIFIED_SINCE'], self
.http_date_format
)
312 # self.log("Checking If-Modified-Since: %s" % since)
315 since
= datetime
.datetime
.strptime(self
.request
['HTTP_IF_MODIFIED_SINCE'], self
.http_date_format_tz
)
316 # self.log("Checking If-Modified-Since: %s" % since)
318 raise HTTPStatusError(400, "Invalid Header If-Modified-Since: '%s' is not a proper date format." % (self
.request
['HTTP_IF_MODIFIED_SINCE']))
319 # self.log("Comparing %s >= %s" % (since, mtime))
321 raise HTTPStatusError(304, "Not Modified")
323 # always local time here because os.path.getmtime is always going to return local as well
324 self
.log("Ignoring OSError: %s" % str(e
))
325 mtime
= datetime
.datetime
.utcnow()
326 if Settings
.supportEtags
:
327 etag
= hashlib
.sha1(str(mtime
)).hexdigest()
328 if self
.request
.get('HTTP_IF_NONE_MATCH','') == etag
:
329 raise HTTPStatusError(304, "Not Modified")
330 self
.response
.headers
['ETag'] = etag
331 self
.response
.headers
['Last-Modified'] = mtime
.strftime(self
.http_date_format_tz
)
333 def _setCompression(self
, file):
334 self
.compression
= Settings
.getCompression(self
.request
.get('HTTP_USER_AGENT',''))
335 if self
.request
.get('HTTP_ACCEPT_ENCODING','').find(self
.compression
) == -1:
336 self
.compression
= None
338 elif self
.response
.headers
.get('Content-Encoding',None) == self
.compression
:
339 # if there is already a compression layer in place
340 # just pass along the right cache file name
341 self
.zfile
= self
.file+"."+self
.compression
342 self
.request
['LIMO_CACHE_FILE'] = self
.file
344 self
.response
.headers
['Content-Encoding'] = self
.compression
345 self
.response
.headers
['Vary'] = 'Accept-Encoding'
346 # log("core: Using compression: %s" % self.compression);
349 url
= [u
for u
in self
.url
if u
is not '']
350 url
[0] = Settings
.cacheDir
351 self
.file = os
.path
.sep
.join(url
)
352 self
._setContentType
(self
.file)
353 self
._setExpires
(self
.file)
354 self
._setLastModified
(self
.file)
355 self
._setCompression
(self
.file)
357 def render(self
, next
=None):
359 if None not in (self
.compression
, self
.zfile
):
360 yield compress
.generate_gzip(compress
.generate_file(self
.file),cache_file
=self
.zfile
)
362 yield compress
.generate_file(self
.file)
364 if Settings
.showVerbose404
:
365 raise HTTPStatusError(404, s
+ " -> OSError: %s" % str(e
))
367 raise HTTPStatusError(404, s
)
369 if Settings
.showVerbose404
:
370 raise HTTPStatusError(404, s
+ " -> IOError: %s" % str(e
))
372 raise HTTPStatusError(404, s
)
374 class HeaderBlock(HTMLBlock
):
375 def render(self
, next
=None):
377 self
.body
= self
.template("header", locals())
378 return HTMLBlock
.render(self
, next
);
380 class RegisterBlock(HTMLBlock
):
382 if self
.args
.get('action', False) == 'register':
383 name
= self
.args
.get('name', False)
384 login
= self
.args
.get('login', False)
385 password
= self
.args
.get('password', False)
386 email
= self
.args
.get('email', False)
387 assert name
and login
and password
and email
, "Missing required fields"
388 assert len(self
.args
['password']) > 4, "Your password was too short."
389 assert self
.args
['password'] == self
.args
['password2'], "Your passwords did not match."
390 assert len(self
.args
['email']) > 4, "Your email was too short."
391 assert self
.args
['email'] == self
.args
['email2'], "Your emails did not match."
392 uid
= User
.createUser(name
, login
, password
, email
)
393 assert uid
, "Failed to create user, database error."
394 self
.session
.addMessage("<div class='success'>You are now registered as %s, your login is '%s'." % (name
, login
))
395 def render(self
, next
=None):
396 self
.title
= "Registration Form"
397 name
= self
.args
.get('name', '')
398 email
= self
.args
.get('email','')
399 email2
= self
.args
.get('email2','')
400 login
= self
.args
.get('login','')
401 self
.body
= self
.template("form-register", locals())
402 return HTMLBlock
.render(self
, next
)
404 class AdminMenuBlock(HTMLBlock
):
405 def render(self
, next
=None):
406 if self
.session
.hasRole('admin'):
407 self
.title
= 'Admin Menu'
408 self
.body
= self
.template("admin-menu", {})
412 yield HTMLBlock
.render(self
, next
)
414 class ModuleBlock(HTMLBlock
):
416 self
.modules
= Query("select mid, name, active from modules")
417 self
.requireCSS("modules.css")
419 if self
.args
.get('action',False):
420 if self
.session
.hasRole('admin'):
421 name
= self
.args
.get('name',False)
422 assert name
, "'name' is required"
423 assert self
.args
['action'] in ("enable","disable"), "Unknown action: %s" % self
.args
['action']
424 if self
.args
['action'] == 'enable':
425 LimoModel().enableModule(name
)
426 self
.session
.addMessage("Module enabled: %s" % name
)
427 elif self
.args
['action'] == 'disable':
428 LimoModel().disableModule(name
)
429 self
.session
.addMessage("Module disabled: %s" % name
)
431 raise HTTPStatusError(403, "Forbidden")
432 def render(self
, next
=None):
433 self
.title
= "Modules"
436 <ul id="module-list" class="module-list">
439 yield self
.template("module-row", {"row": row
})
444 return HTMLBlock
.render(self
, next
)
446 if __name__
== "__main__":