diff --git a/CHANGES b/CHANGES index db09504..94cd8c2 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,12 @@ Flask Kit Changelog Here you can see the Flask Kit evolution. +Version 0.5 +------------ +- Django style Url Routing added to AppFactory - but with no regexp! +- see base/urls.py for example required routes definition structure +- added template-filter registration to AppFactory - define in settings just like extensions or context-processors +- added baseviews file, contains BaseView and ModelView classes, subclass your views from here Version 0.4 ----------- diff --git a/base/filters.py b/base/filters.py new file mode 100644 index 0000000..f551b66 --- /dev/null +++ b/base/filters.py @@ -0,0 +1,38 @@ +from flask import Markup +try: + from markdown2 import markdown as md2 +except ImportError: + from markdown import markdown as md2 +# Jinja + +def date(value): + """Formats datetime object to a yyyy-mm-dd string.""" + return value.strftime('%Y-%m-%d') + + +def date_pretty(value): + """Formats datetime object to a Month dd, yyyy string.""" + return value.strftime('%B %d, %Y') + + +def datetime(value): + """Formats datetime object to a mm-dd-yyyy hh:mm string.""" + return value.strftime('%m-%d-%Y %H:%M') + + +def pluralize(value, one='', many='s'): + """Returns the plural suffix when needed.""" + return one if abs(value) == 1 else many + + +def month_name(value): + """Return month name for a month number.""" + from calendar import month_name + return month_name[value] + + +def markdown(value): + """Convert plain text to HTML.""" + extras = ['fenced-code-blocks', 'wiki-tables'] + return Markup(md2(value, extras=extras)) + diff --git a/base/forms.py b/base/forms.py index 188d436..9fc32e4 100644 --- a/base/forms.py +++ b/base/forms.py @@ -10,8 +10,9 @@ :license: BSD, see LICENSE for more details. """ -from flask.ext.wtf import Form, TextField, Required, PasswordField -from wtforms.validators import Email +from flask.ext.wtf import Form +from wtforms.fields import TextField, PasswordField +from wtforms.validators import Email, DataRequired as Required class LoginForm(Form): diff --git a/base/urls.py b/base/urls.py new file mode 100644 index 0000000..706f49e --- /dev/null +++ b/base/urls.py @@ -0,0 +1,11 @@ +from . import base +from .views import FrontView,LoginView,logout + +routes = [ + ((base), + ('',FrontView.as_view('front_page')), + ('login',LoginView.as_view('login')), + ('logout',logout), + ) + ] + diff --git a/base/views.py b/base/views.py index 67ae13b..ee58acd 100644 --- a/base/views.py +++ b/base/views.py @@ -25,7 +25,6 @@ class FrontView(MethodView): def get(self): return render_template('base/main.html') -base.add_url_rule('', view_func=FrontView.as_view('front_page')) class LoginView(MethodView): @@ -53,7 +52,6 @@ def post(self): return redirect(request.args.get('next') or url_for('base.front_page')) -base.add_url_rule('login', view_func=LoginView.as_view('login')) login_manager.login_view = 'base.login' @@ -70,4 +68,3 @@ def logout(): logout_user() return redirect(url_for('base.front_page')) -base.add_url_rule('logout', view_func=logout, methods=['POST']) diff --git a/baseviews.py b/baseviews.py new file mode 100644 index 0000000..1361a64 --- /dev/null +++ b/baseviews.py @@ -0,0 +1,80 @@ +from flask.views import MethodView +from flask.templating import render_template +from flask.helpers import url_for +from flask import redirect, flash +from wtforms.form import FormMeta + +class BaseView(MethodView): + _template = None + _form = None + _context = {} + _form_obj = None + _obj_id = None + _form_args = {} + + def render(self,**kwargs): + if self._template is None: + return NotImplemented + if kwargs: + self._context.update(kwargs) + if self._form is not None: + + if type(self._form) == FormMeta: + if self._form_obj is not None: + self._context['form'] = self._form(obj=self._form_obj,**self._form_args) + else: + self._context['form'] = self._form(**self._form_args) + if self._obj_id is not None: + self._context['obj_id'] = self._obj_id + else: + self._context['form'] = self._form + choices = self._context.get('choices') + if choices: + self._context['form'].template.template.choices = choices + for f,v in self._form_args.items(): + self._form.__dict__[f].data = v + return render_template(self._template,**self._context) + + def redirect(self,endpoint,**kwargs): + return redirect(url_for(endpoint,**kwargs)) + + def flash(self,*args,**kwargs): + flash(*args,**kwargs) + +class ModelView(BaseView): + _model = None + + def render(self,**kwargs): + if self._model is not None: + if 'model_id' in kwargs: + model_id = kwargs.pop('model_id') + elif self._model.__name__ + '_id' in kwargs: + model_id = kwargs.pop(self._model.__name__+'_id') + else: + model_id = None + if model_id is not None: + self._context['object'] = self.get_by_id(model_id) + else: + self._context['object'] = self._model() + self.context['model'] = self._model + return super(ModelView,self).render(**kwargs) + + def add(self,**kwargs): + tmp = self._model(**kwargs) + tmp.save() + + def update(self,model_id,**kwargs): + tmp = self._model.query.filter_by(self._model.id==model_id).first() + if 'return' in kwargs: + if kwargs.pop('return',None): + rtn = True + else: + rtn = False + for k in kwargs.keys(): + tmp.__dict__[k] = kwargs[k] + tmp.save() + if rtn: return tmp + + def get_by_id(self,model_id): + tmp = self._model.get_by_id(model_id) + return tmp diff --git a/helpers.py b/helpers.py index 5ca976c..450d4a3 100644 --- a/helpers.py +++ b/helpers.py @@ -1,12 +1,8 @@ # -*- coding: utf-8 -*- """ - helpers - ~~~~~~~ - - Implements useful helpers. - - :copyright: (c) 2012 by Roman Semirook. + main.py - where the magic happens + ~~~~~~~~ :license: BSD, see LICENSE for more details. """ @@ -14,19 +10,21 @@ from flask import Flask from werkzeug.utils import import_string +class NoRouteModuleException(Exception): + pass -class NoContextProcessorException(Exception): +class NoTemplateFilterException(Exception): pass +class NoContextProcessorException(Exception): + pass class NoBlueprintException(Exception): pass - class NoExtensionException(Exception): pass - class AppFactory(object): def __init__(self, config, envvar='PROJECT_SETTINGS', bind_db_object=True): @@ -41,7 +39,9 @@ def get_app(self, app_module_name, **kwargs): self._bind_extensions() self._register_blueprints() + self._register_routes() self._register_context_processors() + self._register_template_filters() return self.app @@ -52,6 +52,8 @@ def _get_imported_stuff_by_path(self, path): return module, object_name def _bind_extensions(self): + if self.app.config.get('VERBOSE',False): + print 'binding extensions' for ext_path in self.app.config.get('EXTENSIONS', []): module, e_name = self._get_imported_stuff_by_path(ext_path) if not hasattr(module, e_name): @@ -62,7 +64,19 @@ def _bind_extensions(self): else: ext(self.app) + def _register_template_filters(self): + if self.app.config.get('VERBOSE',False): + print 'registering template filters' + for filter_path in self.app.config.get('TEMPLATE_FILTERS', []): + module, f_name = self._get_imported_stuff_by_path(filter_path) + if hasattr(module, f_name): + self.app.jinja_env.filters[f_name] = getattr(module, f_name) + else: + raise NoTemplateFilterException('No {f_name} template filter found'.format(f_name=f_name)) + def _register_context_processors(self): + if self.app.config.get('VERBOSE',False): + print 'registering template context processors' for processor_path in self.app.config.get('CONTEXT_PROCESSORS', []): module, p_name = self._get_imported_stuff_by_path(processor_path) if hasattr(module, p_name): @@ -71,9 +85,46 @@ def _register_context_processors(self): raise NoContextProcessorException('No {cp_name} context processor found'.format(cp_name=p_name)) def _register_blueprints(self): + if self.app.config.get('VERBOSE',False): + print 'registering blueprints' + self._bp = {} for blueprint_path in self.app.config.get('BLUEPRINTS', []): module, b_name = self._get_imported_stuff_by_path(blueprint_path) - if hasattr(module, b_name): + if hasattr(module, b_name): self.app.register_blueprint(getattr(module, b_name)) + self._bp[b_name] = getattr(module,b_name) + if self.app.config.get('VERBOSE',False): + print 'adding {} to bp'.format(b_name) else: raise NoBlueprintException('No {bp_name} blueprint found'.format(bp_name=b_name)) + + def _register_routes(self): + if self.app.config.get('VERBOSE',False): + print 'starting routing' + for url_module in self.app.config.get('URL_MODULES',[]): + if self.app.config.get('VERBOSE',False): + print url_module + module,r_name = self._get_imported_stuff_by_path(url_module) + if self.app.config.get('VERBOSE',False): + print r_name + print module + if hasattr(module,r_name): + if self.app.config.get('VERBOSE',False): + print 'setting {}'.format(r_name) + self._setup_routes(getattr(module,r_name)) + else: + raise NoRouteModuleException('No {r_name} url module found'.format(r_name=r_name)) + + def _setup_routes(self,routes): + for route in routes: + blueprint,rules = route[0],route[1:] + for pattern, view in rules: + #print 'setting {} {} {}'.format(pattern,view,blueprint[0]) + if type(blueprint) == type(tuple()): + blueprint = blueprint[0] + blueprint.add_url_rule(pattern,view_func=view) + if not blueprint in self.app.blueprints: + if self.app.config.get('VERBOSE',False): + print 'registering {}'.format(str(blueprint)) + self.app.register_blueprint(blueprint) + diff --git a/info/urls.py b/info/urls.py new file mode 100644 index 0000000..9c4106e --- /dev/null +++ b/info/urls.py @@ -0,0 +1,8 @@ +from . import info +from .views import HelpPageView + +routes = [ + ((info), + ('',HelpPageView.as_view('help')), + ) + ] diff --git a/info/views.py b/info/views.py index bdd836c..6e00a67 100644 --- a/info/views.py +++ b/info/views.py @@ -20,4 +20,3 @@ class HelpPageView(MethodView): def get(self): return render_template('info/info_page.html') -info.add_url_rule('', view_func=HelpPageView.as_view('help')) diff --git a/local_settings.py b/local_settings.py new file mode 100644 index 0000000..e1da6bf --- /dev/null +++ b/local_settings.py @@ -0,0 +1,5 @@ + +class LocalConfig: + # put your sensitive local settings here + SECRET_KEY = 'jil' + diff --git a/settings.py b/settings.py index 2babdff..82efc8f 100644 --- a/settings.py +++ b/settings.py @@ -5,36 +5,65 @@ ~~~~~~~~ Global settings for project. - - :copyright: (c) 2012 by Roman Semirook. - :license: BSD, see LICENSE for more details. """ - import os +from local_settings import LocalConfig - -class BaseConfig(object): - DEBUG = False - SECRET_KEY = "MY_VERY_SECRET_KEY" - SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/test.db' +class BaseConfig(LocalConfig): + ADMIN_PER_PAGE = 5 + CODEMIRROR_LANGUAGES = ['python','python2','python3','php','javascript','xml'] + CODEMIRROR_THEME = 'blackboard'#'vivid-chalk'#'3024-night' + SQLALCHEMY_ECHO = True + SQLALCHEMY_COMMIT_ON_TEARDOWN = True CSRF_ENABLED = True ROOT_PATH = os.path.abspath(os.path.dirname(__file__)) - BLUEPRINTS = ['base.base', - 'info.info', - ] + URL_MODULES = [ + 'base.urls.routes', + 'info.urls.routes', + #'admin.urls.routes', + #'auth.urls.routes', + #'blog.urls.routes', + #'member.urls.routes', + #'page.urls.routes', + ] + + BLUEPRINTS = [ + 'base.base', + 'info.info', + #'admin.admin', + #'menu.menu', + #'blog.blog', + #'page.page', + #'auth.auth', + + ] + + EXTENSIONS = [ + 'ext.db', + 'ext.toolbar', + #'ext.pagedown', + #'ext.codemirror', + #'ext.alembic', + ] - EXTENSIONS = ['ext.db', - 'ext.assets', - 'ext.login_manager', - 'ext.gravatar', - 'ext.toolbar', - ] + CONTEXT_PROCESSORS = [ + 'base.context_processors.common_context', + 'base.context_processors.common_forms', + #'menu.context_processors.frontend_nav', + #'menu.context_processors.admin_nav', + #'auth.context_processors.user_context', + #'core.context_processors.add_is_page', + ] - CONTEXT_PROCESSORS = ['base.context_processors.common_context', - 'base.context_processors.navigation', - 'base.context_processors.common_forms', - ] + TEMPLATE_FILTERS = [ + 'base.filters.date', + 'base.filters.date_pretty', + 'base.filters.datetime', + 'base.filters.pluralize', + 'base.filters.month_name', + 'base.filters.markdown', + ] class DevelopmentConfig(BaseConfig): @@ -45,4 +74,3 @@ class DevelopmentConfig(BaseConfig): class TestingConfig(BaseConfig): TESTING = True - SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'