Advertisement
johnmahugu

web2py - model less modules web2pyapps

Aug 14th, 2015
300
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.90 KB | None | 0 0
  1.  
  2.  
  3. Model Less Apps (using data models and modules in web2py)
  4. Recipe by Bruno Rocha (rochacbruno) on 2012-03-05 in Application, Model, Module
  5. Views (7062)Favorite (7)Like (6)Dislike (0)Subscribe (3)
  6.  
  7. Create your datamodels in modules
  8.  
  9. Share
  10.  
  11. Recently many people have asked me why to avoid the use of models in large apps. The main fact is that the models are run each (and all) request, so that all objects, connections, and variables defined there will be global for the whole application.
  12.  
  13. In medium apps that have actions using objects globally there would be no problem, but in apps where size and execution time of actions varies with the complexity, always have all models run may be the bottleneck.
  14.  
  15. A simple example in a single app:
  16. This page needs a lot of data models and functions: http://www.reddit.com/
  17. It does not need: http://www.reddit.com/help/privacypolicy
  18.  
  19. So the best thing is to choose in each action, which data models and functions you want to have available, and thus avoid unnecessary loads.
  20.  
  21. Another case is when we have an ajax call, and this action should only return a JSON, or sometimes only validate a value. in this case do not want to have as many objects Auth, db, crud and all the tables.
  22.  
  23. So, I created a simple base structure for apps without models.
  24.  
  25. A model less approach to web2py applications
  26.  
  27.  
  28. This app uses the following components.
  29.  
  30. modules/appname.py
  31.  
  32. This is the main module will serve as a proxy for other components including db, Auth, Crud, Mail etc.. And this module will also be responsible for loading configuration files that can come from, json, ini, xml or sqlite databases.
  33.  
  34. from gluon.tools import Auth, Crud, Mail
  35. from gluon.dal import DAL
  36. from datamodel.user import User
  37. from gluon.storage import Storage
  38. from gluon import current
  39.  
  40.  
  41. class MyApp(object):
  42. def __init__(self):
  43. self.session, self.request, self.response, self.T = \
  44. current.session, current.request, current.response, current.T
  45. self.config = Storage(db=Storage(),
  46. auth=Storage(),
  47. crud=Storage(),
  48. mail=Storage())
  49. # Global app configs
  50. # here you can choose to load and parse your configs
  51. # from JSON, XML, INI or db
  52. # Also you can put configs in a cache
  53. #### -- LOADING CONFIGS -- ####
  54. self.config.db.uri = "sqlite://myapp.sqlite"
  55. self.config.db.migrate = True
  56. self.config.db.migrate_enabled = True
  57. self.config.db.check_reserved = ['all']
  58. self.config.auth.server = "default"
  59. self.config.auth.formstyle = "divs"
  60. self.config.mail.server = "logging"
  61. self.config.mail.sender = "me@mydomain.com"
  62. self.config.mail.login = "me:1234"
  63. self.config.crud.formstyle = "divs"
  64. #### -- CONFIGS LOADED -- ####
  65.  
  66. def db(self, datamodels=None):
  67. # here we need to avoid redefinition of db
  68. # and allow the inclusion of new entities
  69. if not hasattr(self, "_db"):
  70. self._db = DataBase(self.config, datamodels)
  71. if datamodels:
  72. self._db.define_datamodels(datamodels)
  73. return self._db
  74.  
  75. @property
  76. def auth(self):
  77. # avoid redefinition of Auth
  78. # here you can also include logic to del
  79. # with facebook based in session, request, response
  80. if not hasattr(self, "_auth"):
  81. self._auth = Account(self.db())
  82. return self._auth
  83.  
  84. @property
  85. def crud(self):
  86. # avoid redefinition of Crud
  87. if not hasattr(self, "_crud"):
  88. self._crud = FormCreator(self.db())
  89. return self._crud
  90.  
  91. @property
  92. def mail(self):
  93. # avoid redefinition of Mail
  94. if not hasattr(self, "_mail"):
  95. self._mail = Mailer(self.config)
  96. return self._mail
  97.  
  98.  
  99. class DataBase(DAL):
  100. """
  101. Subclass of DAL
  102. auto configured based in config Storage object
  103. auto instantiate datamodels
  104. """
  105. def __init__(self, config, datamodels=None):
  106. self.config = config
  107. DAL.__init__(self,
  108. **config.db)
  109.  
  110. if datamodels:
  111. self.define_datamodels(datamodels)
  112.  
  113. def define_datamodels(self, datamodels):
  114. # Datamodels will define tables
  115. # datamodel ClassName becomes db attribute
  116. # so you can do
  117. # db.MyEntity.insert(**values)
  118. # db.MyEntity(value="some")
  119. for datamodel in datamodels:
  120. obj = datamodel(self)
  121. self.__setattr__(datamodel.__name__, obj.entity)
  122. if obj.__class__.__name__ == "Account":
  123. self.__setattr__("auth", obj)
  124.  
  125.  
  126. class Account(Auth):
  127. """Auto configured Auth"""
  128. def __init__(self, db):
  129. self.db = db
  130. self.hmac_key = Auth.get_or_create_key()
  131. Auth.__init__(self, self.db, hmac_key=self.hmac_key)
  132. user = User(self)
  133. self.entity = user.entity
  134.  
  135. # READ AUTH CONFIGURATION FROM CONFIG
  136. self.settings.formstyle = self.db.config.auth.formstyle
  137. if self.db.config.auth.server == "default":
  138. self.settings.mailer = Mailer(self.db.config)
  139. else:
  140. self.settings.mailer.server = self.db.config.auth.server
  141. self.settings.mailer.sender = self.db.config.auth.sender
  142. self.settings.mailer.login = self.db.config.auth.login
  143.  
  144.  
  145. class Mailer(Mail):
  146. def __init__(self, config):
  147. Mail.__init__(self)
  148. self.settings.server = config.mail.server
  149. self.settings.sender = config.mail.sender
  150. self.settings.login = config.mail.login
  151.  
  152.  
  153. class FormCreator(Crud):
  154. def __init__(self, db):
  155. Crud.__init__(db)
  156. self.settings.auth = None
  157. self.settings.formstyle = self.db.config.crud.formstyle
  158.  
  159.  
  160.  
  161. modules/basemodel.py
  162.  
  163. This module is an abstraction of the DAL, specifically abstracts the method define_tables. It may seem strange or unnecessary. But I concluded that placing it in a more object-oriented means that the writing is more organized and better reuse of code.
  164.  
  165.  
  166.  
  167. from gluon.dal import DAL
  168. from gluon.tools import Auth
  169.  
  170.  
  171. class BaseModel(object):
  172. """Base Model Class
  173. all define_ methods will be called, then
  174. all set_ methods (hooks) will be called."""
  175.  
  176. hooks = ['set_table',
  177. 'set_validators',
  178. 'set_visibility',
  179. 'set_representation',
  180. 'set_widgets',
  181. 'set_labels',
  182. 'set_comments',
  183. 'set_computations',
  184. 'set_updates',
  185. 'set_fixtures']
  186.  
  187. def __init__(self, db=None, migrate=None, format=None):
  188. self.db = db
  189. assert isinstance(self.db, DAL)
  190. self.config = db.config
  191. if migrate != None:
  192. self.migrate = migrate
  193. elif not hasattr(self, 'migrate'):
  194. self.migrate = self.config.db.migrate
  195. if format != None or not hasattr(self, 'format'):
  196. self.format = format
  197. self.set_properties()
  198. self.check_properties()
  199. self.define_table()
  200. self.define_validators()
  201. self.define_visibility()
  202. self.define_representation()
  203. self.define_widgets()
  204. self.define_labels()
  205. self.define_comments()
  206. self.define_computations()
  207. self.define_updates()
  208. self.pre_load()
  209.  
  210. def check_properties(self):
  211. pass
  212.  
  213. def define_table(self):
  214. fakeauth = Auth(DAL(None))
  215. self.fields.extend([fakeauth.signature])
  216. self.entity = self.db.define_table(self.tablename,
  217. *self.fields,
  218. **dict(migrate=self.migrate,
  219. format=self.format))
  220.  
  221. def define_validators(self):
  222. validators = self.validators if hasattr(self, 'validators') else {}
  223. for field, value in validators.items():
  224. self.entity[field].requires = value
  225.  
  226. def define_visibility(self):
  227. try:
  228. self.entity.is_active.writable = self.entity.is_active.readable = False
  229. except:
  230. pass
  231. visibility = self.visibility if hasattr(self, 'visibility') else {}
  232. for field, value in visibility.items():
  233. self.entity[field].writable, self.entity[field].readable = value
  234.  
  235. def define_representation(self):
  236. representation = self.representation if hasattr(self, 'representation') else {}
  237. for field, value in representation.items():
  238. self.entity[field].represent = value
  239.  
  240. def define_widgets(self):
  241. widgets = self.widgets if hasattr(self, 'widgets') else {}
  242. for field, value in widgets.items():
  243. self.entity[field].widget = value
  244.  
  245. def define_labels(self):
  246. labels = self.labels if hasattr(self, 'labels') else {}
  247. for field, value in labels.items():
  248. self.entity[field].label = value
  249.  
  250. def define_comments(self):
  251. comments = self.comments if hasattr(self, 'comments') else {}
  252. for field, value in comments.items():
  253. self.entity[field].comment = value
  254.  
  255. def define_computations(self):
  256. computations = self.computations if hasattr(self, 'computations') else {}
  257. for field, value in computations.items():
  258. self.entity[field].compute = value
  259.  
  260. def define_updates(self):
  261. updates = self.updates if hasattr(self, 'updates') else {}
  262. for field, value in updates.items():
  263. self.entity[field].update = value
  264.  
  265. def pre_load(self):
  266. for method in self.hooks:
  267. if hasattr(self, method):
  268. self.__getattribute__(method)()
  269.  
  270.  
  271. class BaseAuth(BaseModel):
  272. def __init__(self, auth, migrate=None):
  273. self.auth = auth
  274. assert isinstance(self.auth, Auth)
  275. self.db = auth.db
  276. from gluon import current
  277. self.request = current.request
  278. self.config = self.db.config
  279. self.migrate = migrate or self.config.db.migrate
  280. self.set_properties()
  281. self.define_extra_fields()
  282. self.auth.define_tables(migrate=self.migrate)
  283. self.entity = self.auth.settings.table_user
  284. self.define_validators()
  285. self.hide_all()
  286. self.define_visibility()
  287. self.define_register_visibility()
  288. self.define_profile_visibility()
  289. self.define_representation()
  290. self.define_widgets()
  291. self.define_labels()
  292. self.define_comments()
  293. self.define_computations()
  294. self.define_updates()
  295. self.pre_load()
  296.  
  297. def define_extra_fields(self):
  298. self.auth.settings.extra_fields['auth_user'] = self.fields
  299.  
  300. def hide_all(self):
  301. alwaysvisible = ['first_name', 'last_name', 'password', 'email']
  302. for field in self.entity.fields:
  303. if not field in alwaysvisible:
  304. self.entity[field].writable = self.entity[field].readable = False
  305.  
  306. def define_register_visibility(self):
  307. if 'register' in self.request.args:
  308. register_visibility = self.register_visibility if hasattr(self, 'register_visibility') else {}
  309. for field, value in register_visibility.items():
  310. self.entity[field].writable, self.entity[field].readable = value
  311.  
  312. def define_profile_visibility(self):
  313. if 'profile' in self.request.args:
  314. profile_visibility = self.profile_visibility if hasattr(self, 'profile_visibility') else {}
  315. for field, value in profile_visibility.items():
  316. self.entity[field].writable, self.entity[field].readable = value
  317.  
  318.  
  319.  
  320. modules/datamodel/<some entity>.py
  321.  
  322. Here is where you will create data models, define the fields, validation, fixtures etc ... the API here is different from the normal web2py mode, bmodules/handlers/base.pyut you can still use the same objects and methods.
  323.  
  324.  
  325. from gluon.dal import Field
  326. from basemodel import BaseModel
  327. from gluon.validators import IS_NOT_EMPTY, IS_SLUG
  328. from gluon import current
  329. from plugin_ckeditor import CKEditor
  330.  
  331.  
  332. class Post(BaseModel):
  333. tablename = "blog_post"
  334.  
  335. def set_properties(self):
  336. ckeditor = CKEditor(self.db)
  337. T = current.T
  338. self.fields = [
  339. Field("author", "reference auth_user"),
  340. Field("title", "string", notnull=True),
  341. Field("description", "text"),
  342. Field("body_text", "text", notnull=True),
  343. Field("slug", "text", notnull=True),
  344. ]
  345.  
  346. self.widgets = {
  347. "body_text": ckeditor.widget
  348. }
  349.  
  350. self.visibility = {
  351. "author": (False, False)
  352. }
  353.  
  354. self.representation = {
  355. "body_text": lambda row, value: XML(value)
  356. }
  357.  
  358. self.validators = {
  359. "title": IS_NOT_EMPTY(),
  360. "body_text": IS_NOT_EMPTY()
  361. }
  362.  
  363. self.computations = {
  364. "slug": lambda r: IS_SLUG()(r.title)[0],
  365. }
  366.  
  367. self.labels = {
  368. "title": T("Your post title"),
  369. "description": T("Describe your post (markmin allowed)"),
  370. "body_text": T("The content")
  371. }
  372.  
  373.  
  374.  
  375.  
  376. modules/handlers/base.py
  377.  
  378. This is a rendering engine using the web2py template, just a base class that initializes our handlers with everything you need, here you can inject common objects in to render context, also you can implement cache or extend in any way you want, here you can choose another template language if needed, it is very easy to use chetah, jinja or mako here instead of web2py template (if you really want or need). This structure allows you to easily have an app with multiple themes, and the views can be in any directory or even in the database (I am using it for email templates stored in database).
  379.  
  380.  
  381. rom gluon import URL
  382. from gluon.tools import prettydate
  383.  
  384.  
  385. class Base(object):
  386. def __init__(
  387. self,
  388. hooks=[],
  389. meta=None,
  390. context=None
  391. ):
  392. from gluon.storage import Storage
  393. self.meta = meta or Storage()
  394. self.context = context or Storage()
  395. # you can user alers for response flash
  396. self.context.alerts = []
  397.  
  398. self.context.prettydate = prettydate
  399.  
  400. # hooks call
  401. self.start()
  402. self.build()
  403. self.pre_render()
  404. self.load_menus()
  405.  
  406. # aditional hooks
  407. if not isinstance(hooks, list):
  408. hooks = [hooks]
  409.  
  410. for hook in hooks:
  411. self.__getattribute__(hook)()
  412.  
  413. def start(self):
  414. pass
  415.  
  416. def build(self):
  417. pass
  418.  
  419. def load_menus(self):
  420. self.response.menu = [
  421. (self.T('Home'), False, URL('default', 'index'), []),
  422. (self.T('New post'), False, URL('post', 'new'), []),
  423. ]
  424.  
  425. def pre_render(self):
  426. from gluon import current
  427. self.response = current.response
  428. self.request = current.request
  429. self.session = current.session
  430. self.T = current.T
  431.  
  432. def render(self, view=None):
  433. viewfile = "%s.%s" % (view, self.request.extension)
  434. return self.response.render(viewfile, self.context)
  435.  
  436.  
  437. modules/handlers/<some entity>.py
  438.  
  439. Here is where the logic of action should occur, consult the database, calculate, assemble objects such as forms, tables, etc. .. and here also will check the permissions and authentication, you have to create one handler for each entity of your app, example: contacts, person, article, product...
  440.  
  441.  
  442. from handlers.base import Base
  443. from myapp import MyApp
  444. from datamodel.post import Post as PostModel
  445. from gluon import SQLFORM, URL, redirect
  446.  
  447.  
  448. class Post(Base):
  449. def start(self):
  450. self.app = MyApp()
  451. self.auth = self.app.auth # you need to access this to define users
  452. self.db = self.app.db([PostModel])
  453.  
  454. # this is needed to inject auth in template render
  455. # only needed to use auth.navbar()
  456. self.context.auth = self.auth
  457.  
  458. def list_all(self):
  459. self.context.posts = self.db(self.db.Post).select(orderby=~self.db.Post.created_on)
  460.  
  461. def create_new(self):
  462. # permission is checked here
  463. if self.auth.has_membership("author", self.auth.user_id):
  464. self.db.Post.author.default = self.auth.user_id
  465. self.context.form = SQLFORM(self.db.Post, formstyle='divs').process(onsuccess=lambda form: redirect(URL('show', args=form.vars.id)))
  466. else:
  467. self.context.form = "You can't post, only logged in users, members of 'author' group can post"
  468.  
  469. def edit_post(self, post_id):
  470. post = self.db.Post[post_id]
  471. # permission is checked here
  472. if not post or post.author != self.auth.user_id:
  473. redirect(URL("post", "index"))
  474. self.context.form = SQLFORM(self.db.Post, post.id, formstyle='divs').process(onsuccess=lambda form: redirect(URL('show', args=form.vars.id)))
  475.  
  476. def show(self, post_id):
  477. self.context.post = self.db.Post[post_id]
  478. if not self.context.post:
  479. redirect(URL("post", "index"))
  480.  
  481.  
  482.  
  483.  
  484. controllers/<some entity>.py
  485.  
  486. Here is the entry point to call the handler, will only create an instance of a handler and pass arguments to it, then return will be handler.render() ready and able to cache.
  487.  
  488.  
  489.  
  490. from handlers.post import Post
  491.  
  492.  
  493. def index():
  494. post = Post('list_all')
  495. return post.render("mytheme/listposts")
  496.  
  497.  
  498. def new():
  499. post = Post('create_new')
  500. return post.render("mytheme/newpost")
  501.  
  502.  
  503. def edit():
  504. post = Post()
  505. post.edit_post(request.args(0))
  506. return post.render("mytheme/editpost")
  507.  
  508.  
  509. def show():
  510. post = Post()
  511. post.show(request.args(0))
  512. return post.render("mytheme/showpost")
  513.  
  514.  
  515.  
  516. So the views (template files) will be at views/yourtheme/somefilename
  517.  
  518.  
  519.  
  520. example of the view for show() action
  521.  
  522. {{extend 'layout.html'}}
  523. <div class="row">
  524. <div class="three columns alpha">
  525. <img src="{{=URL('default', 'download', args=post.author.thumbnail)}}" width=100>
  526.  
  527.  
  528. <strong>{{="%(first_name)s %(last_name)s (%(nickname)s)" % post.author}}</strong>
  529.  
  530.  
  531. <small>{{=prettydate(post.created_on)}}</small>
  532. </div>
  533. <div class="eleven columns" style="border-left:1px solid #444;padding:15px;">
  534. <h2><a href="{{=URL('show', args=[post.id, post.slug])}}">{{=post.title}}</a></h2>
  535. {{=XML(post.body_text)}}
  536. </div>
  537. <div class="one columns omega">
  538. {{if auth.user_id == post.author:}}
  539. <a href="{{=URL('edit', args=[post.id, post.slug])}}" class="button">Edit</a>
  540. {{pass}}
  541. </div>
  542. </div>
  543. <hr/>
  544.  
  545.  
  546. I am testing and I found it is very performatic, but, you can help testing it more.
  547.  
  548. The code is here: https://github.com/rochacbruno/web2py_model_less_app
  549. Download the app here: https://github.com/rochacbruno/web2py_model_less_app/downloads
  550.  
  551. This sample is a blog system with just one entity 'blog_post' and also the auth and users, but you can use as a template to create more entities.
  552.  
  553. Can you help testing the gain of performance of this approach?
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement