35 | {% if form_args %}
36 | {{ render_panel_form(form,**form_args) }}
37 | {% else %}
38 | {{ render_panel_form(form) }}
39 | {% endif %}
40 | {% for f in form %}
41 | {% if f.type == 'FormField' %}
42 | {% for t in f %}
43 | {{ t.type }}
44 | {% endfor %}
45 | {% endif %}
46 | {% endfor %}
47 |
48 |
49 | {% endif %}
50 | {% endblock %}
51 | {% block footer_js %}
52 | {{ super() }}
53 |
68 | {% endblock footer_js %}
69 |
--------------------------------------------------------------------------------
/flask_xxl/apps/admin/templates/admin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
25 | {% endif %}
26 | {% block header %}
27 | {% include 'admin/_header.html' with context %}
28 | {% endblock header %}
29 | {% if admin %}
30 |
31 | {% endif %}
32 | {% block messages %}
33 | {% include 'includes/_messages.html' with context %}
34 | {% endblock messages %}
35 | {% block body %}{% endblock body %}
36 | {% block content %}{% endblock content %}
37 |
38 | {% block footer %}{% endblock footer %}
39 |
40 |
41 | {% block footer_js %}
42 |
43 |
45 | {% if form %}
46 | {% set ck = false %}
47 | {% for f in form %}
48 | {% if f.type == 'FormField' %}
49 | {% for field in f %}
50 | {% if field.type == 'CKTextEditorField' %}
51 | {% set ck = true %}
52 | {% endif %}
53 | {% endfor %}
54 | {% endif %}
55 | {% endfor %}
56 | {% if codemirror %}
57 | {#{{ codemirror.include_codemirror() }}#}
58 | {% endif %}
59 |
60 |
63 | {% endif %}
64 | {% endblock footer_js %}
65 |
66 |
67 |
--------------------------------------------------------------------------------
/flask_xxl/apps/auth/models.py:
--------------------------------------------------------------------------------
1 | from flask_xxl.basemodels import BaseMixin
2 | from flask import url_for
3 | from LoginUtils import encrypt_password, check_password
4 | from datetime import datetime
5 | from sqlalchemy import Column,String,Integer,Boolean,Date,DateTime,ForeignKey,UnicodeText,Table
6 | from sqlalchemy.orm import relationship,backref
7 |
8 |
9 | #import sqlalchemy to global namespace
10 |
11 | class UnknownUser(object):
12 | is_unknown = True
13 |
14 | class Role(BaseMixin):
15 |
16 | name = Column(String(255))
17 | can_view = Column(Boolean,default=True,nullable=False)
18 | can_add = Column(Boolean,default=False,nullable=False)
19 | can_edit = Column(Boolean,default=False,nullable=False)
20 | can_delete = Column(Boolean,default=False,nullable=False)
21 |
22 | class User(BaseMixin):
23 |
24 | username = Column(String(255),unique=True,nullable=False)
25 | first_name = Column(String(255),default="")
26 | last_name = Column(String(255),default="")
27 | email = Column(String(255),nullable=False,unique=True)
28 | role_id = Column(Integer,ForeignKey('roles.id'))
29 | role = relationship('Role',backref=backref(
30 | 'users',lazy='dynamic'))
31 | add_date = Column(DateTime,default=datetime.now)
32 | _pw_hash = Column(UnicodeText,nullable=False)
33 | age = Column(Integer)
34 |
35 |
36 | def __init__(self,*args,**kwargs):
37 | if 'first_name' in kwargs:
38 | self.first_name = kwargs.pop('first_name')
39 | if 'last_name' in kwargs:
40 | self.last_name = kwargs.pop('last_name')
41 | if 'email' in kwargs:
42 | self.email = kwargs.pop('email')
43 | if 'role' in kwargs:
44 | self.role = kwargs.pop('role')
45 | if 'role_id' in kwargs:
46 | self.role_id = kwargs.pop('role_id')
47 | if 'password' in kwargs:
48 | self.password = kwargs.pop('password')
49 |
50 | @property
51 | def is_unknown(self):
52 | return False
53 |
54 | def check_password(self, pw):
55 | return check_password(pw,self._pw_hash)
56 |
57 | @classmethod
58 | def get_by_email(cls, email):
59 | return cls.query.filter_by(email=email).first()
60 |
61 | @property
62 | def password(self):
63 | raise ValueError('Private Value!!!!')
64 |
65 | @password.setter
66 | def password(self,pw):
67 | self._pw_hash = encrypt_password(pw)
68 |
69 | @property
70 | def full_name(self):
71 | return '{} {}'.format(self.first_name.title(),self.last_name.title())
72 |
73 | def __str__(self):
74 | if self.first_name != "":
75 | rtn = self.full_name
76 | else:
77 | rtn = self.email
78 | return rtn
79 |
80 | def __repr__(self):
81 | return 'User<{} {}'.format(self.email,self.first_name)
82 |
83 | def _get_absolute_url(self):
84 | return url_for('member.profile',member_id=str(int(self.id)))
85 |
86 | def _get_edit_url(self):
87 | return '#'
88 |
89 |
--------------------------------------------------------------------------------
/flask_xxl/apps/admin/forms.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import Form
2 | from wtforms.ext.sqlalchemy.orm import QuerySelectField
3 | from wtforms import fields, validators,widgets
4 | from flask_pagedown.fields import PageDownField
5 | from flask_codemirror.fields import CodeMirrorField
6 | from ..page.fields import CKTextEditorField
7 | from wtforms.widgets.html5 import DateInput as DateWidget
8 |
9 | #factory = Type.query.all()
10 |
11 | class AddSettingForm(Form):
12 | name = fields.StringField('Setting Name',validators=[validators.InputRequired()])
13 | #type = QuerySelectField('setting type',query_factory=factory,validators=[validators.InputRequired()])
14 | default = fields.StringField('Default Value')
15 | value = fields.StringField('Setting Value')
16 |
17 | class AddSettingTypeForm(Form):
18 | name = fields.StringField('Setting Type Name',validators=[validators.InputRequired()])
19 | widget = fields.StringField('Input Widget')
20 |
21 | class TestForm(Form):
22 | title = fields.StringField('Title')
23 | content = PageDownField('content')
24 |
25 | class BaseTemplateForm(Form):
26 | template = fields.SelectField('base template',validators=[validators.InputRequired()])#,choices=[('a','a'),('b','b'),('c','c')])
27 |
28 | class TemplateBodyFieldForm(Form):
29 | body = CKTextEditorField('body')
30 |
31 |
32 | class AddBlogForm(Form):
33 | name = fields.StringField('Blog Name',validators=[validators.InputRequired()])
34 | title = fields.StringField('Blog Title',validators=[validators.InputRequired()])
35 | slug = fields.StringField('Url Slug')
36 | author_id = fields.HiddenField()
37 | date_added = fields.HiddenField()
38 |
39 |
40 |
41 | class AddPageForm(Form):
42 | date_added = fields.DateField('Publish On:',format="%m-%d-%Y",widget=DateWidget())
43 | date_end = fields.DateField('Expire On:',format="%m-%d-%Y",validators=[validators.Optional()],widget=DateWidget())
44 | name = fields.StringField('Page Name',validators=[validators.InputRequired()])
45 | description = fields.TextAreaField('Description',validators=[validators.Optional()])
46 | slug = fields.StringField('Page Slug',validators=[validators.InputRequired()])
47 | short_url = fields.StringField('Url',validators=[validators.Optional()])
48 | title = fields.StringField('Page Title',validators=[validators.InputRequired()])
49 | add_to_nav = fields.BooleanField('Add to Navbar')
50 | add_sidebar = fields.BooleanField('Add Sidebar')
51 | visible = fields.SelectField(choices=((1,'Publish'),(0,'Draft')))
52 | meta_title = fields.StringField('Meta Title',validators=[validators.InputRequired()])
53 | content = CodeMirrorField('Content',language='xml',config={'lineNumbers':'true'})
54 | template = fields.FormField(BaseTemplateForm,label="Template",separator='_')
55 | blocks = fields.SelectMultipleField(label="blocks",choices=[('a','a'),('b','b'),('c','c')])
56 | category = QuerySelectField('category')
57 | #tags = TagField('Tags')
58 | use_base_template = fields.BooleanField('Use Base Template')
59 | base_template = fields.SelectField('base template',validators=[validators.InputRequired()])
60 | submit = fields.SubmitField('Save')
61 |
--------------------------------------------------------------------------------
/flask_xxl/apps/admin/templates/list.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin.html' %}
2 | {% from '_macros.html' import render_pagination %}
3 | {% macro render_obj_table(headings,objs) %}
4 |
50 | {% endmacro %}
51 | {% macro render_admin_nav(objs) %}
52 | {% set create_url = '#' %}
53 | {% if objs|count > 0 %}
54 | {% if objs[0]._get_create_url %}
55 | {% set create_url = objs[0]._get_create_url() %}
56 | {% endif %}
57 | {% endif %}
58 |
63 | {% endmacro %}
64 | {% block content %}
65 | {% if objs %}
66 | {{ render_admin_nav(objs) }}
67 | {% if headings is defined and objs is defined %}
68 | {{ render_obj_table(headings,objs) }}
69 | {% endif %}
70 | {% if pagination %}
71 | {{ render_pagination(pagination) }}
72 | {% endif %}
73 | {% endif %}
74 | {% endblock %}
75 |
76 |
--------------------------------------------------------------------------------
/flask_xxl/apps/page/models.py:
--------------------------------------------------------------------------------
1 | import flask
2 | import json
3 | import os
4 | import sqlalchemy as sq
5 | from sqlalchemy import orm
6 | sa = sq
7 | import datetime as dt
8 | from flask_xxl.basemodels import AuditMixin,BaseMixin as Model
9 | #try:
10 | # from ext import db as Model
11 | #except ImportError:
12 | # import sys
13 | # print 'the flask blueprint flask_xxl.apps.page needs a database configured as db in the ext module to work correctley'
14 | # sys.exit(1)
15 |
16 | app_base_dir = os.path.abspath(os.path.dirname(__file__))
17 |
18 | class Template(Model):
19 | name = sq.Column(sq.String(255))
20 |
21 | class Block(Model):
22 | name = sq.Column(sq.String(255))
23 |
24 | class Page(Model):
25 |
26 | __tablename__ = 'pages'
27 |
28 | DEFAULT_TEMPLATE = 'page.html'
29 |
30 | id = sq.Column(sq.Integer,primary_key=True)
31 |
32 | parent_id = sa.Column(sa.Integer,sa.ForeignKey('pages.id'))
33 |
34 | children = orm.relationship('Page',backref=orm.backref(
35 | 'parent',remote_side=[id]),lazy='dynamic',primaryjoin='Page.id==Page.parent_id',
36 | cascade='all,delete-orphan')
37 | title = sq.Column(sq.String(255),unique=True,nullable=False)
38 | keywords = sq.Column(sq.Text)
39 | slug = sq.Column(sq.String(255),unique=True,
40 | nullable=False)
41 | template_file = sq.Column(sq.String(255),
42 | nullable=False,default=DEFAULT_TEMPLATE)
43 | meta_title = sa.Column(sa.String(255))
44 | add_right_sidebar = sq.Column(sq.Boolean,default=False)
45 | add_left_sidebar = sq.Column(sq.Boolean,default=False)
46 | add_to_nav = sq.Column(sq.Boolean,default=False)
47 | body_content = sq.Column(sq.Text)
48 |
49 | _current = False
50 |
51 | @property
52 | def has_children(self):
53 | return bool(any(self.children))
54 |
55 | def is_parent_to(self,page=False):
56 | if page:
57 | return page.id == self.parent.id
58 | return self.has_children
59 |
60 | def is_child_of(self,page=None):
61 | if page:
62 | return page in self.children
63 | return self.parent is not None
64 |
65 | def add_child(self,page):
66 | # dont reference any page twice
67 | if not page in self.children:
68 | # dont refrence parent
69 | if self.parent and self.parent.id != page.id:
70 | self.children.append(page)
71 | self.save()
72 |
73 | @property
74 | def navlink(self):
75 | return (self.title,self.get_absolute_url())
76 |
77 | def get_absolute_url(self):
78 | return flask.url_for('.page',slug=self.slug)
79 |
80 | @classmethod
81 | def get_by_slug(cls,slug):
82 | return cls.query().filter(Page.slug==slug).first()
83 |
84 | @classmethod
85 | def get_page_count(cls):
86 | return cls.query().count()
87 |
88 | def __repr__(self):
89 | return '<{}: #{}-{}'.format(self.__class__.__name__,self.id,self.slug)
90 |
91 | def __str__(self):
92 | return unicode(self)
93 |
94 | def __unicode__(self):
95 | return self.title
96 |
97 |
98 | class Category(Model):
99 |
100 | name = sa.Column(sa.String(255),nullable=False,unique=True)
101 | description = sa.Column(sa.Text)
102 |
103 | def __unicode__(self):
104 | return self.name
105 |
--------------------------------------------------------------------------------
/flask_xxl/templates/project/.mrbob.ini:
--------------------------------------------------------------------------------
1 | [template]
2 | pre_render = flask.ext.xxl.hooks:make_db_uri
3 |
4 | [questions]
5 | project.name.question = What will you name this webapp
6 | project.name.required = True
7 | project.name.help = You need a name to start a good project
8 |
9 | project.add_apps.question = would you like me to add any of flask_xxl\'s premade apps to your settings
10 | project.add_apps.post_ask_question = mrbob.hooks:to_boolean
11 |
12 | project.add_page.question = add the page app
13 | project.add_page.help = a small, functional cms app
14 | project.add_page.pre_ask_question = flask_xxl.hooks:add_apps
15 |
16 | project.add_menu.question = add the menu app
17 | project.add_menu.help = a menu app to control the navbars
18 | project.add_menu.pre_ask_question = flask_xxl.hooks:add_apps
19 |
20 | project.add_auth.question = add the auth app
21 | project.add_auth.help = a small, app with User and Role models as well as authentication helpers
22 | project.add_auth.pre_ask_question = flask_xxl.hooks:add_apps
23 |
24 | project.add_admin.question = add the admin app
25 | project.add_admin.help = a small, functional admin app that uses flask_admin as a base
26 | project.add_admin.pre_ask_question = flask_xxl.hooks:add_apps
27 |
28 | project.use_database.question = Will this webapp need database support
29 | project.use_database.required = True
30 | project.use_database.default = no
31 | project.use_database.post_ask_question = mrbob.hooks:to_boolean
32 |
33 | project.db-username.question = Database username **(usually not "your" username)
34 | project.db-username.required = True
35 | project.db-username.pre_ask_question = flask.ext.xxl.hooks:db_related_skip
36 | project.db-username.help = the username for the app to login to the db and add tables
37 |
38 | project.db-password.question = password for user to connect to database
39 | project.db-password.pre_ask_question = flask.ext.xxl.hooks:db_related_skip
40 | project.db-password.command_prompt = getpass:getpass
41 |
42 | project.db-host.question = database host
43 | project.db-host.pre_ask_question = flask.ext.xxl.hooks:db_related_skip
44 | project.db-host.default = localhost
45 |
46 | project.db-port.question = port for database connection
47 | project.db-port.pre_ask_question = flask.ext.xxl.hooks:db_related_skip
48 | project.db-port.default = 3306
49 | project.db-port.post_ask_question = mrbob.hooks:to_integer
50 |
51 | project.db-name.question = name of database to connect to
52 | project.db-name.pre_ask_question = flask.ext.xxl.hooks:db_related_skip
53 |
54 | project.db-driver.question = database driver to use
55 | project.db-driver.pre_ask_question = flask.ext.xxl.hooks:db_related_skip
56 | project.db-driver.post_ask_question = mrbob.hooks:validate_choices
57 | project.db-driver.choices = mysql mysql+pymysql postgresql sqlite
58 |
59 |
60 | project.add_migration.question = do you need migration support
61 | project.add_migration.pre_ask_question = flask.ext.xxl.hooks:db_related_skip
62 |
63 | local_settings.use_captcha.question = does this project need recaptcha support added
64 | local_settings.use_captcha.post_ask_question = mrbob.hooks:to_boolean
65 |
66 | local_settings.recaptcha_public.pre_ask_question = flask_xxl.hooks:check_for_captcha
67 | local_settings.recaptcha_public.question = recaptcha public key
68 |
69 | local_settings.recaptcha_private.pre_ask_question = flask_xxl.hooks:check_for_captcha
70 | local_settings.recaptcha_private.question = recaptcha public key
71 |
--------------------------------------------------------------------------------
/flask_xxl/mr_bob.py:
--------------------------------------------------------------------------------
1 | from flask_script import Manager, prompt_bool
2 | import os
3 | from mrbob import cli
4 | import mrbob
5 | from jinja2.loaders import FileSystemLoader
6 | import sys
7 |
8 |
9 | manager = Manager(usage="performs mr.bob template runs")
10 |
11 | EXT_ROOT = os.path.abspath(os.path.dirname(__file__))
12 |
13 | MRBOB_TEMPLATE_DIR = os.path.join(EXT_ROOT,'templates')
14 | mrbob.rendering.jinja2_env.loader = FileSystemLoader(MRBOB_TEMPLATE_DIR)
15 |
16 |
17 | def get_all_files(dirname,seen=None):
18 | rtn = set()
19 | if seen is None:
20 | seen = set()
21 | seen.add(dirname)
22 | for dname,dlist,flist in os.walk(dirname):
23 | for itm in flist:
24 | if not itm.endswith('.pyc'):
25 | rtn.add(os.path.join(dname,itm))
26 | if len(dlist) == 0:
27 | return rtn
28 | else:
29 | for d in dlist:
30 | pth = os.path.join(dname,d)
31 | if not pth in seen:
32 | rtn.update(get_all_files(pth,seen))
33 |
34 | remove_dir = lambda dirname,path: path.split(dirname)[-1]
35 | remove_bob = lambda x: x.replace('.bob','')
36 |
37 |
38 | def get_templates():
39 | return [name for name in os.listdir(MRBOB_TEMPLATE_DIR) if not name.startswith('_') and not name.endswith('.pyc')]
40 |
41 | @manager.command
42 | def print_template_dir():
43 | print MRBOB_TEMPLATE_DIR
44 |
45 | @manager.command
46 | def print_templates():
47 | print ', '.join(map(str,get_templates()))
48 |
49 | @manager.option('template',nargs='?',default=None)
50 | def print_template_files(template=None):
51 | if template is None:
52 | print 'please provide a template name'
53 | return
54 | else:
55 | if not template in get_templates():
56 | print 'Error! {} is not installed'.format(template)
57 | return
58 | print '\n'.join(
59 | map(
60 | str,
61 | [remove_bob(remove_dir(
62 | MRBOB_TEMPLATE_DIR,x
63 | )) for x in get_all_files(
64 | os.path.join(
65 | MRBOB_TEMPLATE_DIR,template
66 | )
67 | ) if not x is None and not x.endswith('.pyc')
68 | ]
69 | )
70 | )
71 | _template_dir = lambda name: os.path.join(MRBOB_TEMPLATE_DIR,name)
72 |
73 | @manager.option("-t","--testing",action='store_true')
74 | def start_project(testing=False):
75 | template_dir = _template_dir('project')
76 | if testing:
77 | target_dir = './testing'
78 | else:
79 | target_dir = os.curdir
80 | if target_dir == os.curdir:
81 | if not prompt_bool('not testing, are you sure you want to continue...'):
82 | sys.exit(0)
83 | list_questions = False
84 | non_interactive = False
85 | args = [template_dir,'-O',target_dir,'-v','-w']
86 | cli.main(args)
87 |
88 | @manager.option("-t","--testing",action="store_true")
89 | def add_blueprint(testing=False):
90 | template_dir = _template_dir('blueprint')
91 | if testing:
92 | target_dir = './testing'
93 | else:
94 | target_dir = os.curdir
95 | if target_dir == os.curdir:
96 | if not prompt_bool('not testing, are you sure you want to continue...'):
97 | sys.exit(0)
98 | list_questions = False
99 | non_interactive = False
100 | args = [template_dir,'-O',target_dir,'-v','-w']
101 | cli.main(args)
102 |
103 | @manager.command
104 | def angular_app():
105 | template_dir = _template_dir('angular_app')
106 | target_dir = os.path.join(os.curdir,'static','js')
107 | if not os.path.exists(target_dir):
108 | os.makedirs(target_dir)
109 | list_questions = False
110 | non_interactive = False
111 | args = [template_dir,'-O',target_dir,'-v','-w']
112 | cli.main(args)
113 |
114 | @manager.command
115 | def angular_service():
116 | template_dir = _template_dir('angular_service')
117 | target_dir = os.path.join(os.curdir,'static','js')
118 | if not os.path.exists(target_dir):
119 | os.makedirs(target_dir)
120 | list_questions = False
121 | non_interactive = False
122 | args = [template_dir,'-O',target_dir,'-v','-w']
123 | cli.main(args)
124 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/flask_xxl/apps/admin/cfg.py:
--------------------------------------------------------------------------------
1 | from admin import admin
2 | from importlib import import_module
3 | from flask import current_app
4 | from .models import Setting,Type
5 | from flask.ext.wtf import Form
6 |
7 |
8 | class BaseSettings(object):
9 | _settings = []
10 | _values = {}
11 | _form = None
12 | _defaults = {}
13 | _cfg = ''
14 |
15 | def __init__(self):
16 | self._cfg = current_app.config
17 | for itm in self._settings:
18 | if not itm in self._values:
19 | try:
20 | self.set_setting(itm,self._defaults[itm])
21 | except KeyError:
22 | raise ValueError('Need to be given a value or default for {}'.format(itm))
23 | self.set_setting(itm,self._cfg[itm])
24 |
25 | def _get_setting_field(self,setting,field_type=None):
26 | pass
27 |
28 | def _get_form(self):
29 | raise NotImplementedError
30 |
31 | @property
32 | def form(self):
33 | return self._get_form()
34 |
35 | def set_setting(self,setting,value):
36 | if setting in self._settings:
37 | self._values[setting] = value
38 |
39 | def get_setting(self,setting,default=None):
40 | if setting in self._settings:
41 | return self._values[setting]
42 | return default
43 |
44 | def apply_settings(self,**kwargs):
45 | if len(kwargs) > 0:
46 | for k in kwargs:
47 | self.set_setting(k,kwargs[k])
48 | for setting in self._settings:
49 | if not setting.startswith('_'):
50 | self._cfg[setting.upper()] = self.get_setting(setting)
51 |
52 | def _get_default_field(self,setting,fields):
53 | return fields.__dict__['StringField']
54 |
55 | class SiteSettings(BaseSettings):
56 | _settings = [
57 | 'RECAPTCHA_PUBLIC_KEY',
58 | 'RECAPTCHA_PRIVATE_KEY',
59 | 'ADMIN_PER_PAGE',
60 | 'CODEMIRROR_LANGUAGES',
61 | 'CODEMIRROR_THEME',
62 | 'BLUEPRINTS',
63 | 'EXTENSIONS',
64 | 'TEMPLATE_FILTERS',
65 | 'CONTEXT_PROCESSORS',
66 | ]
67 | _defaults = {
68 | 'RECAPTCHA_PUBLIC_KEY':'',
69 | 'RECAPTCHA_PRIVATE_KEY':'',
70 | 'ADMIN_PER_PAGE':'',
71 | 'CODEMIRROR_LANGUAGES':['python'],
72 | 'CODEMIRROR_THEME':'3024-year',
73 | 'BLUEPRINTS':'',
74 | 'EXTENSIONS':'',
75 | 'TEMPLATE_FILTERS':'',
76 | 'CONTEXT_PROCESSORS':'',
77 | }
78 | def _get_setting_field(self,setting,field_type=None):
79 | fields = import_module('wtforms.fields')
80 | if field_type is not None:
81 | field = fields.__dict__[field_type]
82 | else:
83 | from .models import Setting
84 | s = Setting.query.filter(Setting.name==setting).first()
85 | if s is None:
86 | field = self._get_default_field(setting,fields)
87 | else:
88 | field = fields.__dict__[s.type.field_type]
89 | return field
90 |
91 | def _get_form(self):
92 | form_args = {}
93 | for itm in self._settings:
94 | form_args[itm] = self._get_setting_field(itm)()
95 | self._form = type(
96 | 'EditSiteSettingsForm',(Form,),form_args
97 | )
98 | return self._form
99 |
100 |
101 |
102 |
103 |
104 |
105 | @admin.before_app_request
106 | def add_settings():
107 | from app import app
108 | settings = app.config.copy()
109 | CACHED_SETTINGS = [
110 | 'RECAPTCHA_PUBLIC_KEY',
111 | 'RECAPTCHA_PRIVATE_KEY',
112 | 'ADMIN_PER_PAGE',
113 | 'CODEMIRROR_LANGUAGES',
114 | 'CODEMIRROR_THEME',
115 | 'BLUEPRINTS',
116 | 'EXTENSIONS',
117 | 'TEMPLATE_FILTERS',
118 | 'CONTEXT_PROCESSORS',
119 | ]
120 | for itm in CACHED_SETTINGS:
121 | setting = Setting.query.filter(Setting.name==itm).first()
122 | if setting is None:
123 | t = Type.query.filter(Type.name==type(settings[itm])).first()
124 | value = settings.get(itm,None)
125 | if value is None:
126 | value = ''
127 | if t is None:
128 | t = Type(type(settings[itm]))
129 | t.save()
130 | setting = Setting(
131 | name=itm,type=t,value=value
132 | )
133 | setting.save()
134 |
--------------------------------------------------------------------------------
/flask_xxl/templates/project/+project.name+/settings.py.bob:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | {{% macro add_blueprint(name) %}}
3 | '{{{ name }}}.{{{ name }}}',
4 | {{% endmacro %}}
5 | {{% macro render_dburi(driver,host,user,pw,db,port='3306') %}}
6 | SQLALCHEMY_DATABASE_URI = '{{{ driver }}}://{{{ user }}}:{{{ pw }}}@{{{ host }}}:{{{ port }}}/{{{ db }}}'
7 | {{% endmacro %}}
8 | {{% macro render_app_urls(app) %}}
9 | 'flask_xxl.apps.{{{ app }}}.urls.routes',
10 | {{% endmacro %}}
11 | {{% macro render_install_app(app) %}}
12 | 'flask_xxl.apps.{{{ app }}}.{{{ app }}}',
13 | {{% endmacro %}}
14 |
15 | """
16 | settings
17 | ~~~~~~~~
18 | Global settings for project.
19 | """
20 | import os
21 | from local_settings import LocalConfig
22 |
23 | class BaseConfig(LocalConfig):
24 | ROOT_PATH = os.path.abspath(os.path.dirname(__file__))
25 | CSRF_ENABLED = True
26 | #ADMIN_PER_PAGE = 5
27 | #CODEMIRROR_LANGUAGES = ['python','python2','python3','php','javascript','xml']
28 | #CODEMIRROR_THEME = 'blackboard'#'vivid-chalk'#'3024-night'
29 | #SQLALCHEMY_ECHO = True
30 | #SQLALCHEMY_COMMIT_ON_TEARDOWN = True
31 |
32 |
33 | #####################################################
34 | # Define url modules with routes defined inside
35 | #
36 | # in bp.urls.py file make routes
37 | # routes = [
38 | # ((bp_name),
39 | # ('/url_rule',ViewClass.as_view('endpoint')),
40 | # ('/next_url',view_func)),
41 | # ]
42 | #
43 | #####################################################
44 | URL_MODULES = [
45 | '{{{project.name}}}.urls.routes',
46 | {{% if project.add_apps -%}}
47 | {{%- for app in ['admin','page','auth','menu'] -%}}
48 | {{%- if project.get('add_%s' % app) -%}}
49 | {{{ render_app_urls(app) }}}
50 | {{%- endif %}}
51 | {{%- endfor %}}
52 | {{%- endif %}}
53 | #'flask.ext.xxl.apps.admin.urls.routes',
54 | #'flask.ext.xxl.apps.auth.urls.routes',
55 | #'flask.ext.xxl.apps.page.urls.routes',
56 | ]
57 |
58 | #####################################################
59 | # define sub dirs as blueprints
60 | # in __init__.py file define blueprint like this:
61 | # from flask import BluePrint
62 | # blueprint = BluePrint('blueprint',__name__,
63 | # url_prefix='/blueprint',
64 | # template_dir='templates')
65 | #####################################################
66 | BLUEPRINTS = [
67 | '{{{project.name}}}.{{{project.name}}}',
68 | {{%- if project.add_apps %}}
69 | {{%- for app in ['admin','page','menu','auth'] %}}
70 | {{%- if project.get('add_%s' % app) %}}
71 | {{{ render_install_app(app) }}}
72 | {{% endif -%}}
73 | {{% endfor -%}}
74 | {{% endif -%}}
75 | #'flask.ext.xxl.apps.admin.admin',
76 | #'flask.ext.xxl.apps.menu.menu',
77 | #'flask.ext.xxl.apps.page.page',
78 | #'flask.ext.xxl.apps.auth.auth',
79 | ]
80 |
81 | ####################################################
82 | # define flask extensions to use in your app
83 | # in ext.py import and create instance of extensions
84 | # from flask.ext.name import Extension
85 | #
86 | # extension = Extension()
87 | #####################################################
88 |
89 | EXTENSIONS = [
90 | 'ext.db',
91 | {{% if project.add_migration -%}}
92 | 'ext.alembic',
93 | {{% else %}}
94 | #'ext.alembic',
95 | {{%- endif %}}
96 | #'ext.toolbar',
97 | #'ext.pagedown',
98 | #'ext.codemirror',
99 |
100 | ]
101 |
102 | ###################################################
103 | # context processors add global variables or
104 | # functions to your templates
105 | #
106 | # define like this:
107 | # def my_processor_name():
108 | # def the_func_to_add():
109 | # #w/e you want to do
110 | # return {name:the_func_to_add}
111 | # then in templates name is available
112 | ######################################################
113 |
114 | CONTEXT_PROCESSORS = [
115 | 'flask.ext.xxl.context_processors.add_get_model',
116 | #'flask.ext.xxl.context_processors.add_is_page',
117 | #'flask.ext.xxl.context_processors.common_context',
118 | #'flask.ext.xxl.context_processors.common_forms',
119 | #'flask.ext.xxl.apps.menu.context_processors.frontend_nav',
120 | #'flask.ext.xxl.apps.menu.context_processors.admin_nav',
121 | #'flask.ext.xxl.apps.auth.context_processors.user_context',
122 | ]
123 |
124 | TEMPLATE_FILTERS = [
125 | 'flask.ext.xxl.filters.date',
126 | 'flask.ext.xxl.filters.date_pretty',
127 | 'flask.ext.xxl.filters.datetime',
128 | 'flask.ext.xxl.filters.pluralize',
129 | 'flask.ext.xxl.filters.month_name',
130 | 'flask.ext.xxl.filters.markdown',
131 | ]
132 |
133 |
134 | def get_choices(option):
135 | return BaseConfig.DYNAMIC_SETTINGS[option]
136 |
137 |
138 | class DevelopmentConfig(BaseConfig):
139 | DEBUG = True
140 | DEBUG_TB_PROFILER_ENABLED = True
141 | DEBUG_TB_INTERCEPT_REDIRECTS = False
142 |
143 |
144 | class TestingConfig(BaseConfig):
145 | TESTING = True
146 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Flask-XXL
2 | #### - A best practices approach to creating larger web apps with Flask, in an attempt to make Flask feel like it is as capable, if not more, than __Django__.
3 | this is an amazing program best in the world
4 | [](https://badge.fury.io/py/flaskxxl)
5 |
6 | [](https://www.codementor.io/jstacoder)
7 |
8 | [](https://gitter.im/jstacoder/flask-xxl?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
9 |
10 | _to see this in a real world example take a look at my other projects_ [Flask-Cms](https://github.com/jstacoder/flask-cms) or [Flask-Ide](https://github.com/jstacoder/flask-ide)
11 |
12 |
13 |
14 | ## What this provides:
15 |
16 | - Installable blueprints
17 | - any blueprints listed in your settings under `BLUEPRINTS`
18 | will be imported and registered on your app
19 | if that blueprint is a package, any files it contains named `models.py` or `views.py` will be imported as well,
20 | so no more need to manualy import your views and models
21 | giving odd errors if you dont do it in the exact correct order!!
22 |
23 | - basemodels.py
24 | - with a sqlalchemy compatible BaseMixin class
25 | - provides many useful CRUD operations, IE: model.save(), model.delete()
26 | - `BaseMixin` generates `__tablename__` automaticlly
27 | - `BaseMixin` adds an auto incrementing `id` field, as the primary_key to each model
28 | - `BaseMixin.session` is current model classes session
29 | - `BaseMixin.engine` is the current db engine
30 | - `BaseMixin.query` is the models sqlalchemy query from its session
31 | - `BaseMixin.get_all()` `->` function to return all of a model
32 | - `BaseMixin.get(*args,**kwargs)` `->` get single model by attr values, mainly for id=x
33 |
34 | - baseviews.py
35 | - with a BaseView class that is subclassed from Flask.views.MethodView to allow easy definition of view responses to get and post requests.
36 | - BaseView also has many builtin helpers/imports to speed development, ie:
37 | - BaseView.render() calls:
38 | `render_template(BaseView._template,**BaseView._context)`
39 | easily define either or both in the class variable
40 | section of the class and then add or change whatever you need to
41 | ie: maybe based on logic that happens during request processing.
42 | for example:
43 | ```python
44 | class ExampleView(BaseView):
45 | _context = {
46 | 'some_flag':True,
47 | }
48 |
49 | def get(self,new_flag=False):
50 | if new_flag:
51 | self._context['new_flag'] = new_flag
52 | self._context['some_flag'] = False
53 | return self.render()
54 |
55 | ```
56 |
57 | - `BaseView.redirect(endpoint)`
58 | is a reimplementation of `flask.helpers.redirect` which allows you to directly enter the
59 | endpoint, so you dont have to run it through `url_for()` first.
60 |
61 | - `BaseView.get_env()` -> returns the current jinja2_env
62 |
63 | - `BaseView.form_validated()` -> returns true if all forms validate
64 |
65 | - __namespaces imported into BaseView__:
66 | `BaseView.flash == flask.flash`
67 |
68 |
69 |
70 |
71 | - many builtin template globals(context_processors) to use.
72 | ie:
73 |
74 | - `get_block(block_id)` <-- requires use of flask.ext.xxl.apps.blog
75 | * add blocks of html/jinja2/template helpers
76 | into the db and access from within templates
77 | great for things like header navs or sidebar widgets
78 |
79 | - `get_icon(icon_name,icon_lib)` <-- requires use of flask.ext.xxl.apps.blog
80 | - flask.ext.xxl.apps.blog comes with 8 icon librarys!!!
81 | * Glyphicon
82 | * Font Awesome
83 | * Mfg_Labs
84 | * Elusive icons
85 | * Genericons
86 | * and more ...
87 |
88 | access any icon anywhere in your templates! even from cms blocks!!!
89 |
90 | - `get_model(model_name,blueprint_name)`
91 | - access any model class from any template (currently only supports sqlalchemy models)
92 |
93 | - `get_button(name)`
94 | - create buttons in the cms and access from within templates
95 |
96 | - AppFactory class with many hooks into settings file (makes use of settings file similar to django)
97 | - settings like:
98 | - CONTEXT_PROCESSORS
99 | - TEMPLATE_FILTERS
100 | - URL_ROUTE_MODULES
101 | - INSTALLED_BLUEPRINTS etc..
102 |
103 | - new revamped url routing scheme, use a urls.py file in each blueprint to
104 | define the url routes for the blueprint. reference the blueprint and the url
105 | route module in the settings file to registar onto the app upon instantiation.
106 |
107 | define routes like this:
108 |
109 | file: urls.py
110 | ```python
111 | from blueprint import blueprint
112 | from .views import ViewName,SecondView
113 |
114 | routes = [
115 | ((blueprint_name,)
116 | ('/url',ViewName.as_View('view_name')),
117 | ('/another',SecondView.as_view('second_view')),
118 | )
119 | ]
120 | ```
121 | it basicly is like using `app.add_url_rule()` method, you
122 | just dont have to add `view_func=ViewName.as_view(endpoint)`
123 | or at least the `view_func=` part.
124 |
125 |
126 | - easily start a new project or extend an old one
127 | with the flaskxxl-manage.py command line helper tool
128 | - to start a project from scratch
129 | `$ flaskxxl-manage.py start-project`
130 |
131 | - to add to an existing project
132 | `$ flaskxxl-manage.py start-blueprint`
133 |
134 |
135 | [](https://bitdeli.com/free "Bitdeli Badge")
136 |
137 | for more fun checkout the [wiki](https://github.com/jstacoder/flask-xxl/wiki)
138 |
--------------------------------------------------------------------------------
/tests/test_settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | settings
5 | ~~~~~~~~
6 |
7 | Global settings for project.
8 | """
9 | import os
10 | from .test_local_settings import LocalConfig
11 |
12 | class BaseConfig(LocalConfig):
13 | SYSTEM_MESSAGE_CATEGORIES = [
14 | 'success' # 0 - GREEN
15 | 'info', # 1 - BLUE
16 | 'warning', # 2 - YELLOW
17 | 'danger', # 3 - RED
18 | ]
19 |
20 | ADMIN_PER_PAGE = 5
21 | CODEMIRROR_LANGUAGES = ['python','python2','python3','php','javascript','xml','jinja2']
22 | CODEMIRROR_THEME = 'blackboard'#'vivid-chalk'#'3024-night'
23 | SQLALCHEMY_ECHO = True
24 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True
25 | CSRF_ENABLED = True
26 | ROOT_PATH = os.path.abspath(os.path.dirname(__file__))
27 |
28 | URL_MODULES = [
29 | #'core.urls.routes',
30 | #'admin.urls.routes',
31 | 'flask.ext.xxl.apps.auth.urls.routes',
32 | 'flask.ext.xxl.apps.page.urls.routes',
33 | 'flask.ext.xxl.apps.admin.urls.routes',
34 | #'blog.urls.routes',
35 | #'member.urls.routes',
36 | #'page.urls.routes',
37 | #'fileviewer.urls.routes',
38 | ]
39 |
40 | BLUEPRINTS = [
41 | #'core.core',
42 | #'member.member',
43 | #'admin.admin',
44 | #'menu.menu',
45 | #'blog.blog',
46 | #'page.page',
47 | 'flask.ext.xxl.apps.auth.auth',
48 | 'flask.ext.xxl.page.auth.auth',
49 | 'flask.ext.xxl.admin.auth.auth',
50 | #'auth.auth',
51 | #'fileviewer.fileviewer',
52 |
53 | ]
54 |
55 | EXTENSIONS = [
56 | #'ext.db',
57 | #'ext.toolbar',
58 | #'ext.pagedown',
59 | #'ext.codemirror',
60 | #'ext.alembic',
61 | ]
62 |
63 | CONTEXT_PROCESSORS = [
64 | #'core.context_processors.common_context',
65 | #'core.context_processors.common_forms',
66 | #'menu.context_processors.frontend_nav',
67 | #'menu.context_processors.admin_nav',
68 | #'auth.context_processors.user_context',
69 | #'core.context_processors.add_is_page',
70 | #'core.context_processors.add_is_list',
71 | #'core.context_processors.add_get_model',
72 | #'core.context_processors.add_get_button',
73 | #'core.context_processors.add_get_icon',
74 | #'core.context_processors.get_context',
75 | #'core.context_processors.add_get_block',
76 | #'core.context_processors.add_urlfor',
77 | #'core.context_processors.add_layouts',
78 | #'core.context_processors.add_layout_mode',
79 | #'page.context_processors.add_page_self_context',
80 | #'menu.context_processors.get_navbar',
81 | #'menu.context_processors._add_navbar',
82 | #'menu.context_processors.sidebar',
83 | #'make_base.base',
84 | #'auth.context_processors.auth_context',
85 | #'blog.context_processors.add_admin_head',
86 | #'core.context_processors.add_size_converters',
87 |
88 | ]
89 |
90 | TEMPLATE_FILTERS = [
91 | 'flask.ext.xxl.filters.date',
92 | 'flask.ext.xxl.filters.date_pretty',
93 | 'flask.ext.xxl.filters.datetime',
94 | 'flask.ext.xxl.filters.pluralize',
95 | 'flask.ext.xxl.filters.month_name',
96 | 'flask.ext.xxl.filters.markdown',
97 | #'core.context_processors.fix_body',
98 | #'core.filters.split',
99 | #'blog.filters.markdown',
100 | ]
101 |
102 | CONTACT_FORM_SETTINGS = {
103 | 'HEADING':'Send Us a message',
104 | 'SUBHEADING':'Or a Comment',
105 | 'OPTIONS':(
106 | ('test','opt1'),
107 | ('test2','opt2'),
108 | ('test3','opt3'),
109 | ('test4','opt4'),
110 | ('test5','opt5'),
111 | ('test6','opt6'),
112 | ),
113 | 'SUBMIT_TEXT':'Send to Us',
114 | 'COMPANY_TITLE':'Level2designs',
115 | 'COMPANY_ADDRESS':{
116 | 'NAME':'level2designs',
117 | 'STREET':'1045 w katella',
118 | 'CITY':'Orange',
119 | 'STATE':'CA',
120 | 'ZIP':'92804',
121 | },
122 | 'COMPANY_PHONE':'714-783-6369',
123 | 'CONTACT_NAME':'Roux',
124 | 'CONTACT_EMAIL':'kyle@level2designs.com',
125 | }
126 |
127 |
128 | # populates header option in creating a cms page
129 | # should be tuples of (name,file)
130 | NAVBAR_TEMPLATE_FILES = (
131 | ('bootstrap-std','navbars/bs_std.html'),
132 | ('bootstrap-inverse','navbars/bs_inverse.html'),
133 | ('blog','navbars/blog.html'),
134 | ('clean','navbars/clean.html')
135 | )
136 |
137 | DEFAULT_NAVBAR = 'clean'
138 |
139 | LAYOUT_FILES = {
140 | 'blog':'layouts/1col_leftsidebar.html',
141 | 'post_form':'layouts/1col_rightsidebar.html',
142 | 'one_col_left':'layouts/1col_leftsidebar.html',
143 | 'one_col_right':'layouts/1col_rightsidebar.html',
144 | 'two_col_left':'layouts/2col_leftsidebar.html',
145 | 'two_col_right':'layouts/2col_rightsidebar.html',
146 | 'three_col_left':'layouts/3col_leftsidebar.html',
147 |
148 | }
149 |
150 | BASE_TEMPLATE_FILES = [
151 | ('one_col_left','1_col_left.html'),
152 | ('one_col_right','1_col_right.html'),
153 | ('two_col_left','2_col_left.html'),
154 | ('two_col_right','2_col_right.html'),
155 | ('three_col','3_col.html'),
156 | ]
157 |
158 | #BLOG_SIDEBAR_LEFT = False
159 | #BLOG_SIDEBAR_RIGHT = True
160 | BLOG_SIDEBAR_LEFT = True
161 | #BLOG_SIDEBAR_RIGHT = False
162 | BLOG_TITLE = 'Dynamic'
163 | BLOG_CONTENT = 'some text to put into my
Blog'
164 |
165 | DEFAULT_ICON_LIBRARY = 'octicon'
166 |
167 | def get_choices():
168 | return BaseConfig.CONTACT_FORM_SETTINGS['OPTIONS']
169 |
170 |
171 | class DevelopmentConfig(BaseConfig):
172 | DEBUG = True
173 | DEBUG_TB_PROFILER_ENABLED = True
174 | DEBUG_TB_INTERCEPT_REDIRECTS = False
175 |
176 |
177 | class TestingConfig(BaseConfig):
178 | TESTING = True
179 | SQLALCHEMY_ECHO = False
180 | SQLALCHEMY_DATABASE_URI = 'mysql://test:test@174.140.227.137:3306/test_test5'
181 |
182 |
--------------------------------------------------------------------------------
/tests/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | settings
5 | ~~~~~~~~
6 |
7 | Global settings for project.
8 | """
9 | import os
10 | from .local_settings import LocalConfig
11 |
12 | class BaseConfig(LocalConfig):
13 | SYSTEM_MESSAGE_CATEGORIES = [
14 | 'success' # 0 - GREEN
15 | 'info', # 1 - BLUE
16 | 'warning', # 2 - YELLOW
17 | 'danger', # 3 - RED
18 | ]
19 |
20 | ADMIN_PER_PAGE = 5
21 | CODEMIRROR_LANGUAGES = ['python','python2','python3','php','javascript','xml','jinja2']
22 | CODEMIRROR_THEME = 'blackboard'#'vivid-chalk'#'3024-night'
23 | SQLALCHEMY_ECHO = True
24 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True
25 | CSRF_ENABLED = True
26 | ROOT_PATH = os.path.abspath(os.path.dirname(__file__))
27 |
28 | URL_MODULES = [
29 | #'core.urls.routes',
30 | #'admin.urls.routes',
31 | 'flask.ext.xxl.apps.auth.urls.routes',
32 | 'flask.ext.xxl.apps.page.urls.routes',
33 | 'flask.ext.xxl.apps.admin.urls.routes',
34 | #'blog.urls.routes',
35 | #'member.urls.routes',
36 | #'page.urls.routes',
37 | #'fileviewer.urls.routes',
38 | ]
39 |
40 | BLUEPRINTS = [
41 | #'core.core',
42 | #'member.member',
43 | #'admin.admin',
44 | #'menu.menu',
45 | #'blog.blog',
46 | #'page.page',
47 | 'flask.ext.xxl.apps.auth.auth',
48 | 'flask.ext.xxl.apps.page.page',
49 | 'flask.ext.xxl.apps.admin.admin',
50 | #'auth.auth',
51 | #'fileviewer.fileviewer',
52 |
53 | ]
54 |
55 | EXTENSIONS = [
56 | #'ext.db',
57 | #'ext.toolbar',
58 | #'ext.pagedown',
59 | #'ext.codemirror',
60 | #'ext.alembic',
61 | ]
62 |
63 | CONTEXT_PROCESSORS = [
64 | #'core.context_processors.common_context',
65 | #'core.context_processors.common_forms',
66 | #'menu.context_processors.frontend_nav',
67 | #'menu.context_processors.admin_nav',
68 | #'auth.context_processors.user_context',
69 | #'core.context_processors.add_is_page',
70 | #'core.context_processors.add_is_list',
71 | #'core.context_processors.add_get_model',
72 | #'core.context_processors.add_get_button',
73 | #'core.context_processors.add_get_icon',
74 | #'core.context_processors.get_context',
75 | #'core.context_processors.add_get_block',
76 | #'core.context_processors.add_urlfor',
77 | #'core.context_processors.add_layouts',
78 | #'core.context_processors.add_layout_mode',
79 | #'page.context_processors.add_page_self_context',
80 | #'menu.context_processors.get_navbar',
81 | #'menu.context_processors._add_navbar',
82 | #'menu.context_processors.sidebar',
83 | #'make_base.base',
84 | #'auth.context_processors.auth_context',
85 | #'blog.context_processors.add_admin_head',
86 | #'core.context_processors.add_size_converters',
87 |
88 | ]
89 |
90 | TEMPLATE_FILTERS = [
91 | 'flask.ext.xxl.filters.date',
92 | 'flask.ext.xxl.filters.date_pretty',
93 | 'flask.ext.xxl.filters.datetime',
94 | 'flask.ext.xxl.filters.pluralize',
95 | 'flask.ext.xxl.filters.month_name',
96 | 'flask.ext.xxl.filters.markdown',
97 | #'core.context_processors.fix_body',
98 | #'core.filters.split',
99 | #'blog.filters.markdown',
100 | ]
101 |
102 | CONTACT_FORM_SETTINGS = {
103 | 'HEADING':'Send Us a message',
104 | 'SUBHEADING':'Or a Comment',
105 | 'OPTIONS':(
106 | ('test','opt1'),
107 | ('test2','opt2'),
108 | ('test3','opt3'),
109 | ('test4','opt4'),
110 | ('test5','opt5'),
111 | ('test6','opt6'),
112 | ),
113 | 'SUBMIT_TEXT':'Send to Us',
114 | 'COMPANY_TITLE':'Level2designs',
115 | 'COMPANY_ADDRESS':{
116 | 'NAME':'level2designs',
117 | 'STREET':'1045 w katella',
118 | 'CITY':'Orange',
119 | 'STATE':'CA',
120 | 'ZIP':'92804',
121 | },
122 | 'COMPANY_PHONE':'714-783-6369',
123 | 'CONTACT_NAME':'Roux',
124 | 'CONTACT_EMAIL':'kyle@level2designs.com',
125 | }
126 |
127 |
128 | # populates header option in creating a cms page
129 | # should be tuples of (name,file)
130 | NAVBAR_TEMPLATE_FILES = (
131 | ('bootstrap-std','navbars/bs_std.html'),
132 | ('bootstrap-inverse','navbars/bs_inverse.html'),
133 | ('blog','navbars/blog.html'),
134 | ('clean','navbars/clean.html')
135 | )
136 |
137 | DEFAULT_NAVBAR = 'clean'
138 |
139 | LAYOUT_FILES = {
140 | 'blog':'layouts/1col_leftsidebar.html',
141 | 'post_form':'layouts/1col_rightsidebar.html',
142 | 'one_col_left':'layouts/1col_leftsidebar.html',
143 | 'one_col_right':'layouts/1col_rightsidebar.html',
144 | 'two_col_left':'layouts/2col_leftsidebar.html',
145 | 'two_col_right':'layouts/2col_rightsidebar.html',
146 | 'three_col_left':'layouts/3col_leftsidebar.html',
147 |
148 | }
149 |
150 | BASE_TEMPLATE_FILES = [
151 | ('one_col_left','1_col_left.html'),
152 | ('one_col_right','1_col_right.html'),
153 | ('two_col_left','2_col_left.html'),
154 | ('two_col_right','2_col_right.html'),
155 | ('three_col','3_col.html'),
156 | ]
157 |
158 | #BLOG_SIDEBAR_LEFT = False
159 | #BLOG_SIDEBAR_RIGHT = True
160 | BLOG_SIDEBAR_LEFT = True
161 | #BLOG_SIDEBAR_RIGHT = False
162 | BLOG_TITLE = 'Dynamic'
163 | BLOG_CONTENT = 'some text to put into my
Blog'
164 |
165 | DEFAULT_ICON_LIBRARY = 'octicon'
166 |
167 | VERBOSE = True
168 |
169 | def get_choices():
170 | return BaseConfig.CONTACT_FORM_SETTINGS['OPTIONS']
171 |
172 |
173 | class DevelopmentConfig(BaseConfig):
174 | DEBUG = True
175 | DEBUG_TB_PROFILER_ENABLED = True
176 | DEBUG_TB_INTERCEPT_REDIRECTS = False
177 |
178 |
179 | class TestingConfig(BaseConfig):
180 | TESTING = True
181 | SQLALCHEMY_ECHO = False
182 | SQLALCHEMY_DATABASE_URI = 'mysql://test:test@174.140.227.137:3306/test_test5'
183 |
184 |
--------------------------------------------------------------------------------
/flask_xxl/basemodels.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | basemodels.py
4 | ~~~~~~~~~~~
5 | """
6 | import os
7 | from functools import wraps
8 | from werkzeug.local import LocalProxy
9 | from flask import current_app,g
10 | try:
11 | from flask import _app_ctx_stack as stack
12 | except ImportError:
13 | from flask import _request_ctx_stack as stack
14 |
15 | from inflection import underscore, pluralize
16 | from sqlalchemy.ext.declarative import as_declarative, declarative_base, declared_attr
17 | from sqlalchemy.ext.declarative.api import DeclarativeMeta
18 | from sqlalchemy.orm import scoped_session, sessionmaker
19 | from sqlalchemy import UniqueConstraint,Column,Integer,Text,String,Date,DateTime,ForeignKey,func,create_engine
20 |
21 | # classproperty decorator
22 | class classproperty(object):
23 | def __init__(self, getter):
24 | self.getter = getter
25 | def __get__(self, instance, owner):
26 | return self.getter(owner)
27 |
28 |
29 | echo_sql = lambda: os.environ.get('DATABASE_URI') and current_app.config.get('SQLALCHEMY_ECHO',False)
30 | get_engine = lambda: create_engine(os.environ.get('DATABASE_URI') or current_app.config['DATABASE_URI'],echo=echo_sql())
31 |
32 | Base = declarative_base()
33 |
34 |
35 | class SQLAlchemyMissingException(Exception):
36 | pass
37 |
38 | class BaseMixin(Base):
39 | __table_args__ = {
40 | 'extend_existing':True
41 | }
42 | __abstract__ = True
43 | _session = None
44 | _engine = None
45 | _query = None
46 | _meta = None
47 |
48 | def __init__(self,*args,**kwargs):
49 | super(BaseMixin,self).__init__(*args,**kwargs)
50 | metadata = BaseMixin.metadata or Base.metadata
51 | self.metadata = BaseMixin.metadata = metadata
52 |
53 | @classproperty
54 | def engine(cls):
55 | if BaseMixin._engine is None:
56 | BaseMixin._engine = get_engine()
57 | BaseMixin._engine.echo = echo_sql()
58 | return BaseMixin._engine
59 |
60 | @declared_attr
61 | def id(self):
62 | return Column(Integer,primary_key=True)
63 |
64 | @classproperty
65 | def session(cls):
66 | if BaseMixin._session is None:
67 | BaseMixin._session = scoped_session(sessionmaker(bind=cls.engine))()
68 | return BaseMixin._session
69 |
70 | @classproperty
71 | def query(cls):
72 | if cls._query is None:
73 | cls._query = cls.session.query(cls)
74 | return cls._query
75 |
76 | @declared_attr
77 | def __tablename__(self):
78 | return underscore(pluralize(self.__name__))
79 |
80 | @classmethod
81 | def get_by_id(cls, id):
82 | if any(
83 | (isinstance(id, basestring) and id.isdigit(),
84 | isinstance(id, (int, float))),
85 | ):
86 | return cls.query.get(int(id))
87 | return None
88 |
89 | @classmethod
90 | def get_all(cls):
91 | return cls.query.all()
92 |
93 | @classmethod
94 | def create(cls, **kwargs):
95 | instance = cls(**kwargs)
96 | return instance.save()
97 |
98 | def update(self, commit=True, **kwargs):
99 | for attr, value in kwargs.iteritems():
100 | setattr(self, attr, value)
101 | return commit and self.save() or self
102 |
103 | def save(self,commit=True,testing=False):
104 | try:
105 | self.session.add(self)
106 | if commit:
107 | self.session.commit()
108 | except Exception, e:
109 | if not testing:
110 | raise e
111 | print e.message
112 | return False
113 | return self
114 |
115 | def delete(self, commit=True):
116 | self.session.delete(self)
117 | return commit and self.session.commit()
118 |
119 | @property
120 | def absolute_url(self):
121 | return self._get_absolute_url()
122 |
123 | def _get_absolute_url(self):
124 | raise NotImplementedError('need to define _get_absolute_url')
125 |
126 | @classmethod
127 | def get_all_columns(cls,exclude=['id']):
128 | if not 'id' in exclude:
129 | exclude.append('id')
130 | rtn = []
131 | for col in cls.__table__.c._all_columns:
132 | if not col.name in exclude and not col.name.endswith('id'):
133 | rtn.append((col.name,_clean_name(col.name)))
134 | for attr in dir(cls):
135 | if not attr in exclude:
136 | if not attr in [x[0] for x in rtn]:
137 | if not attr.startswith('_') and not attr.endswith('id'):
138 | if not callable(getattr(cls,attr)):
139 | rtn.append((attr,_clean_name(attr)))
140 | return rtn
141 |
142 |
143 | class AuditMixin(object):
144 | __abstract__ = True
145 |
146 | @declared_attr
147 | def date_added(self):
148 | return sq.Column(sq.DateTime,default=dt.now)
149 |
150 | @declared_attr
151 | def date_modified(self):
152 | return sq.Column(sq.DateTime,onupdate=dt.now)
153 |
154 | def _clean_name(name):
155 | names = name.split('_')
156 | if len(names) > 1:
157 | if len(names) == 2:
158 | name = names[0].title() + ' ' + names[-1].title()
159 | else:
160 | name = names[0].title() + ' {} '.format(' '.join(map(str,names[1:-1]))) + names[-1].title()
161 | else:
162 | name = names[0].title()
163 | return name
164 |
165 | class DBObject(object):
166 | _session = None
167 | _engine = None
168 | _metadata = None
169 |
170 | @staticmethod
171 | def check_ctx():
172 | return stack.top is not None
173 |
174 | @classproperty
175 | def session(cls):
176 | sess = cls._session
177 | if sess is None:
178 | sess = DBObject._session = scoped_session(sessionmaker(bind=DBObject.engine))
179 | return sess
180 |
181 | @classproperty
182 | def engine(cls):
183 | engine = cls._engine
184 | if engine is None:
185 | engine = cls._engine = cls.check_ctx() and create_engine(current_app.config.get('DATABASE_URI') or os.environ.get('DATABASE_URI'))
186 | return engine
187 |
188 | @classproperty
189 | def metadata(cls):
190 | meta = cls._metadata
191 | if meta is None:
192 | meta = cls._metadata = BaseMixin.metadata
193 | meta.bind = meta.bind if meta.bind else cls.engine
194 | return meta
195 |
196 | db = LocalProxy(DBObject)
197 |
--------------------------------------------------------------------------------
/flask_xxl/apps/page/views.py:
--------------------------------------------------------------------------------
1 | from flask_xxl.baseviews import BaseView,ModelAPIView
2 | import flask
3 | from . import page
4 | from .forms import TestForm,ContactUsForm
5 | from .models import Page
6 |
7 |
8 | #class TestView(ModelAPIView):
9 | # _model = Page
10 |
11 | class ContactFormView(BaseView):
12 | _template = 'contact.html'
13 | _form = ContactUsForm
14 | _context = {}
15 |
16 | def get(self):
17 | return self.render()
18 |
19 | def post(self):
20 | return self.render()
21 |
22 | class PageSlugView(BaseView):
23 | _template = 'page.html'
24 | _context = {}
25 |
26 | def get(self,slug):
27 | file_name = None
28 | if slug.endswith('.html'):
29 | # first try to load the html file
30 | if slug in page.jinja_loader.list_templates():
31 | file_name = slug
32 | else:
33 | slug = slug.split('.html')[0]
34 | try:
35 | url = flask.url_for('page.'+slug)
36 | return self.redirect(url)
37 | except:
38 | pass
39 | test = TestForm()
40 | content = ''
41 | page_name = ''
42 | if '.html' in slug:
43 | slug = slug.split('.html')[0]
44 | page = Page._session.query(Page).filter(Page.slug==slug).all()
45 | if page is not None:
46 | if type(page) == list and len(page) != 0:
47 | page = page[0]
48 | content = page.body_content
49 | template_file = page.template_file if getattr(page,'template_file',False) and os.path.exists(page.template_file) else page.DEFAULT_TEMPLATE
50 | page_name = page.name
51 | title = page.title
52 | meta_title = page.meta_title
53 | elif file_name is not None:
54 | template_file = file_name
55 | title = slug
56 | meta_title = slug
57 | else:
58 | template_file = slug + '.html'
59 | title = slug
60 | meta_title = slug
61 | self._template = template_file
62 | self._context['content'] = content
63 | self._context['page_title'] = title
64 | self._context['page_name'] = page_name
65 | self._context['test'] = test
66 | return self.render()
67 |
68 |
69 | class AddPageView(BaseView):
70 | pass
71 |
72 | class PagesView(BaseView):
73 | pass
74 |
75 | '''
76 | admin_required = ''
77 |
78 | class BasePageView(BaseView):
79 | _template = ''
80 | _context = {}
81 | _base_template = 'aabout.html'
82 | _parent_template = None
83 |
84 | def render(self):
85 | return render_template_string(self._template,**self._context)
86 |
87 | def get(self,slug):
88 | from auth.models import User
89 | from page.models import Page
90 | self._page = Page.get_by_slug(slug)
91 | self.frontend_nav()
92 | self._context['content'] = self._page.content
93 | if self._page.use_base_template:
94 | if self._page.template is not None:
95 | self._base_template = self._page.body
96 | self._process_base_template()
97 |
98 | def post(self,slug):
99 | from auth.models import User
100 | from page.models import Page
101 | self._page = Page.get_by_slug(slug)
102 | self.frontend_nav()
103 | self._context['content'] = self._page.content
104 | if self._page.use_base_template:
105 | if self._page.template is not None:
106 | self._base_template = self._page.body
107 | self._process_base_template()
108 |
109 | def _process_base_template(self):
110 | self._template = "{{template}}"
111 | self._template = render_template_string(self._template,template=self._base_template)
112 | self._template = "{% extends '" + self._template + "' %}
{% block body %}{% endblock body %}
"
113 |
114 | def frontend_nav(self):
115 | self._setup_nav()
116 |
117 | def _setup_sidebar(self):
118 | if self._page.add_to_sidebar:
119 | sidebar_data = []
120 | pages = self._page.query.all()
121 | for page in pages:
122 | if page.add_to_sidebar:
123 | sidebar_data.append(
124 | ((page.name,'pages.page',dict(slug=page.slug)))
125 | )
126 | #self._context['sidebar_data'] = ('Title',sidebar_data)
127 |
128 | def _setup_nav(self):
129 | self._context['nav_links'] = []
130 | pages = self._page.query.all()
131 | for page in pages:
132 | if page.add_to_nav:
133 | self._context['nav_links'].append(
134 | (('page.pages',dict(slug=page.slug)),'{}'.format(page.name))
135 | )
136 | self._context['nav_title'] = '{}'.format(self._page.title)
137 | #self._setup_sidebar()
138 |
139 |
140 | class PagesView(BasePageView):
141 |
142 | def get(self,slug):
143 | super(PagesView,self).get(slug)
144 | #blocks = self._page.blocks.all()
145 | #self._page.template.process_blocks(blocks)
146 | return self.render()
147 |
148 | def post(self,slug):
149 | super(PagesView,self).post(slug)
150 | data = request.form.get('content')
151 | if data != self._page.content:
152 | self._page.content = data
153 | self._page.update()
154 | return self.render()
155 |
156 |
157 | class AddPageView(BaseView):
158 |
159 | def get(self):
160 | from .models import Page
161 | from blog.models import Tag
162 | data = dict(
163 | date_added=request.args.get('date_added',None),
164 | date_end=request.args.get('date_end',None),
165 | name=request.args.get('name',None),
166 | description=request.args.get('description',None),
167 | slug=request.args.get('slug',None),
168 | short_url=request.args.get('short_url',None),
169 | title=request.args.get('title',None),
170 | add_to_nav=request.args.get('add_to_nav',None),
171 | add_sidebar=request.args.get('add_sidebar',None),
172 | visible=request.args.get('visible',None),
173 | meta_title=request.args.get('meta_title',None),
174 | content=request.args.get('content',None),
175 | template=request.args.get('template',None),
176 | category=request.args.get('category',None),
177 | tags=request.args.get('tags',None),
178 | use_base_template=request.args.get('use_base_template',None),
179 | )
180 | p = Page.query.filter(Page.name==data['name']).first()
181 | if p is not None:
182 | res = 0
183 | else:
184 | tags = [x.name for x in Tag.query.all()]
185 | for tag in data['tags']:
186 | if not tag in tags:
187 | t = Tag()
188 | t.name = tag
189 | t.save()
190 | p = Page()
191 | p.name = data.get('name')
192 | p.date_added = data.get('date_added')
193 | p.date_end = data.get('date_end',None)
194 | p.description = data.get('description',None)
195 | p.slug = data.get('slug',None)
196 | p.short_url = data.get('short_url',None)
197 | p.title = data.get('title',None)
198 | nav = data.get('add_to_nav',1)
199 | if str(nav).lower() == 'y':
200 | nav = 1
201 | else:
202 | nav = 0
203 | p.add_to_nav = nav
204 | sidebar = data.get('add_sidebar',0)
205 | if str(sidebar).lower() == 'y':
206 | sidebar = 1
207 | else:
208 | sidebar = 0
209 | p.add_sidebar = sidebar
210 | p.visible = data.get('visible',None)
211 | p.meta_title = data.get('meta_title',None)
212 | p.content = data.get('content',None)
213 | p.template = data.get('template',None)
214 | p.category = data.get('category',None)
215 | p.tags = data.get('tags',None)
216 | p.use_base_template = data.get('use_base_template',None)
217 | p.save()
218 | res = 1
219 | return jsonify(result=res,content=data['content'])
220 | '''
221 |
--------------------------------------------------------------------------------
/flask_xxl/baseviews.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | from flask.views import MethodView
4 | from flask.templating import render_template
5 | from flask.helpers import url_for
6 | from flask import redirect, flash, request
7 | from inflection import pluralize
8 | from wtforms.form import FormMeta
9 | from .basemodels import classproperty
10 |
11 |
12 | is_verbose = lambda: os.environ.get('VERBOSE') and True or False
13 |
14 | class Flasher(object):
15 | DEFAULT_CATEGORY = 'warning'
16 | WARNING_CLASS = 'warning'
17 | INFO_CLASS = 'info'
18 | SUCCESS_CLASS = 'success'
19 | ERROR_CLASS = 'danger'
20 |
21 | def __init__(self,default=None,class_map=None):
22 | if default:
23 | self.DEFAULT_CATEGORY = default
24 | if class_map:
25 | self._set_classes(class_map)
26 |
27 | def _set_classes(self,class_map):
28 | default_map = {
29 | 'success':self.SUCCESS_CLASS,
30 | 'warning':self.WARNING_CLASS,
31 | 'error':self.ERROR_CLASS,
32 | 'info':self.INFO_CLASS
33 | }
34 | for k in class_map:
35 | tmp = class_map.get(k)
36 | if tmp is not None:
37 | default_map[k] = tmp
38 |
39 |
40 | def flash(self,msg,cat=None):
41 | cat = cat or self.DEFAULT_CATEGORY
42 | return flash(msg,cat)
43 |
44 | def add_warning(self,msg):
45 | return self.flash(msg,self.WARNING_CLASS)
46 |
47 | def add_error(self,msg):
48 | return self.flash(msg,self.ERROR_CLASS)
49 |
50 | def add_info(self,msg):
51 | return self.flash(msg,self.INFO_CLASS)
52 |
53 | def add_success(self,msg):
54 | return self.flash(msg,self.SUCCESS_CLASS)
55 |
56 |
57 | class PostViewAddon(object):
58 | def _process_post(self):
59 | self.post_data = ((request.data and json.loads(request.data)) if not request.form else dict(request.form.items())) if not request.mimetype == 'application/json' else request.json
60 |
61 |
62 | class BaseView(MethodView):
63 | _template = None
64 | _form = None
65 | _context = {}
66 | _form_obj = None
67 | _obj_id = None
68 | _form_args = {}
69 | _default_view_routes = {}
70 | _flasher = None
71 | _flasher_class_map = None
72 |
73 | def __init__(self,*args,**kwargs):
74 | super(BaseView,self).__init__(*args,**kwargs)
75 | if self._flasher_class_map is not None:
76 | self._flasher = Flasher(class_map=self._flasher_class_map)
77 | else:
78 | self._flasher = Flasher()
79 |
80 |
81 | def flash(self,msg):
82 | return self.flasher.flash(msg)
83 |
84 | def success(self,msg):
85 | return self._flasher.add_success(msg)
86 |
87 | def warning(self,msg):
88 | return self._flasher.add_warning(msg)
89 |
90 | def info(self,msg):
91 | return self._flasher.add_info(msg)
92 |
93 | def error(self,msg):
94 | return self._flasher.add_danger(msg)
95 |
96 | @classmethod
97 | def _add_default_routes(cls,app=None):
98 | for route,endpoint in cls._default_view_routes.items():
99 | if is_verbose():
100 | print 'attaching',route,'to view func',endpoint
101 | app.add_url_rule(route,endpoint,view_func=cls.as_view(endpoint))
102 |
103 | def render(self,**kwargs):
104 | if self._template is None:
105 | return NotImplemented
106 | if kwargs:
107 | self._context.update(kwargs)
108 | if self._form is not None:
109 |
110 | if type(self._form) == FormMeta:
111 | if self._form_obj is not None:
112 | self._context['form'] = self._form(obj=self._form_obj,**self._form_args)
113 | else:
114 | self._context['form'] = self._form(**self._form_args)
115 | if self._obj_id is not None:
116 | self._context['obj_id'] = self._obj_id
117 | else:
118 | self._context['form'] = self._form
119 | choices = self._context.get('choices')
120 | if choices:
121 | for field in self._context['form']:
122 | if hasattr(field,field.__name__) and hasattr(getattr(field,field.__name__),field.__name__):
123 | inner_field = getattr(getattr(field,field.__name__),getattr(field.__name__))
124 | if hasattr(inner_field,'choices'):
125 | setattr(inner_field,'choices',choices)
126 | for f,v in self._form_args.items():
127 | self._form.__dict__[f].data = v
128 | return render_template(self._template,**self._context)
129 |
130 | def redirect(self,endpoint,**kwargs):
131 | if not kwargs.pop('raw',False):
132 | return redirect(url_for(endpoint,**kwargs))
133 | return redirect(endpoint,**kwargs)
134 |
135 | def form_validated(self):
136 | if self._form:
137 | return self._form().validate()
138 | return False
139 |
140 | def get_form_data(self):
141 | result = {}
142 | for field in self._form():
143 | name = field.name
144 | if '_' in field.name:
145 | if not field.name.startswith('_'):
146 | if not field.name.endswith('_'):
147 | if field.name.split('_')[0] == field.name.split('_')[1]:
148 | name = field.name.split('_')[0]
149 | result[name] = field.data
150 | return result
151 |
152 | def get_env(self):
153 | from flask import current_app
154 | return current_app.create_jinja_environment()
155 |
156 | @property
157 | def flasher(self):
158 | return self._flasher
159 |
160 |
161 | class ModelView(BaseView):
162 | # ModelView is an abstract class
163 | # just an interface really
164 | # to use the ModelView create your own view class
165 | # and use this as the parent class, and
166 | # set its _model class attr to the class to wrap ie:
167 | #
168 | # class UserModelView(ModelView):
169 | # _model = User
170 | _model = None
171 |
172 | def render(self,**kwargs):
173 | alt_model_id = '{0}_id'.format(self._model.__name__.lower())
174 | if self._model is not None:
175 | if 'model_id' in kwargs:
176 | model_id = kwargs.pop('model_id')
177 | elif alt_model_id in kwargs:
178 | model_id = kwargs.pop('alt_model_id')
179 | else:
180 | model_id = None
181 | if model_id is not None:
182 | self._context['object'] = self.get_by_id(model_id)
183 | else:
184 | self._context['object'] = self._model()
185 | self._context['model'] = self._model
186 | return super(ModelView,self).render(**kwargs)
187 |
188 | def add(self,**kwargs):
189 | tmp = self._model(**kwargs)
190 | tmp.save()
191 | return tmp
192 |
193 | def update(self,model_id,**kwargs):
194 | tmp = self._model.query.filter_by(self._model.id==model_id).first()
195 | if 'return' in kwargs:
196 | if kwargs.pop('return',None):
197 | rtn = True
198 | else:
199 | rtn = False
200 | for k in kwargs.keys():
201 | tmp.__dict__[k] = kwargs[k]
202 | tmp.save()
203 | if rtn: return tmp
204 |
205 | def get_all(self):
206 | return self._model.get_all()
207 |
208 | def get_by_id(self,model_id):
209 | return self._model.get_by_id(model_id)
210 |
211 | class AddModelView(ModelView,PostViewAddon):
212 | _success_endpoint = None
213 | _success_message = 'You successfully added an item'
214 |
215 | def get(self):
216 | return self.render()
217 |
218 | def post(self):
219 | self._process_post()
220 | self._context['obj'] = self._model(**self.post_data).save()
221 | self.success(self._success_message)
222 | return self.redirect(self._success_endpoint or '.index')
223 |
224 |
225 | def AddModelApiView(ModelView,PostViewAddon):
226 | def post(self):
227 | self._process_post()
228 | return jsonify(**self.add(**self.post_data).to_json())
229 |
230 | class ListModelView(ModelView):
231 | def get(self):
232 | name = pluralize(self._model.__name__)
233 | return jsonify(name=[m.to_json() for m in self.get_all()])
234 |
235 | class ViewModelView(ModelView):
236 | def get(self,item_id):
237 | m = self.get_by_id(item_id)
238 | return jsonify(**m.to_json())
239 |
240 | class ModelAPIView(ModelView):
241 | __abstract__ = True
242 |
243 | @classproperty
244 | def _default_view_routes(cls):
245 | if cls is ModelAPIView:
246 | return {}
247 | name = cls.__name__.lower()
248 | _default_view_routes = {
249 | '/{}/list'.format(name):'{}-list'.format(name),
250 | '/{}/detail'.format(name):'{}-detail'.format(name),
251 | '/{}/edit'.format(name):'{}-edit'.format(name),
252 | }
253 | return _default_view_routes
254 |
255 |
256 | def render(self,**kwargs):
257 | old_rtn = super(ModelAPIView,self).render(**kwargs)
258 | rtn = make_response(json.dumps(self._context))
259 | rtn.headers['Content-Type'] = 'application/json'
260 | return rtn
261 |
262 |
263 |
--------------------------------------------------------------------------------
/flask_xxl/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | main.py - where the magic happens
5 | ~~~~~~~~
6 | :license: BSD, see LICENSE for more details.
7 | """
8 | import jinja2_highlight
9 | import sys
10 | import os
11 | from flask import Flask,views
12 | from werkzeug.utils import import_string,find_modules
13 | import jinja2_highlight
14 | from .baseviews import is_verbose
15 |
16 |
17 | class MyFlask(Flask):
18 | jinja_options = dict(Flask.jinja_options)
19 | jinja_options.setdefault('extensions',
20 | []).append('jinja2_highlight.HighlightExtension')
21 |
22 | class NoRouteModuleException(Exception):
23 | pass
24 |
25 | class NoTemplateFilterException(Exception):
26 | pass
27 |
28 | class NoContextProcessorException(Exception):
29 | pass
30 |
31 | class NoBlueprintException(Exception):
32 | pass
33 |
34 | class NoExtensionException(Exception):
35 | pass
36 |
37 | class NoInstalledBlueprintsSettingException(Exception):
38 | pass
39 |
40 | class AppFactory(object):
41 | _routes_registered = None
42 |
43 | def __init__(self, config, envvar='PROJECT_SETTINGS', bind_db_object=True):
44 | self.app_config = config
45 | self.app_envvar = os.environ.get(envvar, False)
46 | self.bind_db_object = bind_db_object
47 |
48 | def get_app(self, app_module_name, **kwargs):
49 | self.app = MyFlask(app_module_name, **kwargs)
50 | self.app.config.from_object(self.app_config)
51 | self.app.config.from_envvar(self.app_envvar, silent=True)
52 | self.app.config['VERBOSE'] = is_verbose()
53 |
54 |
55 | self._set_path()
56 | self._bind_extensions()
57 | #self._register_blueprints()
58 | self._register_routes()
59 | self._load_models()
60 | self._load_views()
61 | self._register_context_processors()
62 | self._register_template_filters()
63 | self._register_template_extensions()
64 | self._register_middlewares()
65 |
66 | return self.app
67 |
68 | def _register_middlewares(self):
69 | for middleware_path in self.app.config.get('MIDDLEWARE', []):
70 | module_name, middleware_name = middleware_path.rsplit('.', 1)
71 | module_name, _ = self._get_imported_stuff_by_path(middleware_path)
72 | middleware = getattr(module_name, middleware_name, None)
73 | if middleware is not None:
74 | self.app.wsgi_app = middleware(self.app.wsgi_app)
75 |
76 | def _set_path(self):
77 | sys.path.append(self.app.config.get('ROOT_PATH',''))
78 |
79 | def _is_public_attr(self,name):
80 | return not name.startswith('_')
81 |
82 | def _get_imported_stuff_by_path(self, path):
83 | module_name, object_name = path.rsplit('.', 1)
84 | module = import_string(module_name)
85 | return module, object_name
86 |
87 | def _load_resource(self,typename):
88 | bp_settings_path = ((self.app.config.get('BLUEPRINTS',None) and 'BLUEPRINTS') or (self.app.config.get('INSTALLED_BLUEPRINTS',None) and 'INSTALLED_BLUEPRINTS') or False)
89 | if not bp_settings_path:
90 | raise NoInstalledBlueprintsSettingException('You must have a setting for either INSTALLED_BLUEPRINTS or BLUEPRINTS')
91 | for blueprint_path in self.app.config.get(bp_settings_path, []):
92 | module_name, object_name = blueprint_path.rsplit('.', 1)
93 | blueprint_module, bp_name = self._get_imported_stuff_by_path(blueprint_path)
94 | blueprint = getattr(blueprint_module,bp_name)
95 | modules = find_modules(module_name)
96 | for module in modules:
97 | if typename in module:
98 | mod = import_string(module)
99 | for itm in dir(mod):
100 | cls = getattr(mod,itm)
101 | if self._is_public_attr(itm) and\
102 | itm[0] == str(itm[0]).upper() and\
103 | 'class' in str(cls) and\
104 | 'view' in str(itm).lower():
105 |
106 | if hasattr(cls,'_add_default_routes') and\
107 | getattr(cls,'_default_view_routes'):
108 | if is_verbose():
109 | print 'getting default routes for ',cls.__name__
110 | getattr(cls,'_add_default_routes')(app=blueprint or self.app)
111 |
112 |
113 |
114 | def _load_views(self):
115 | return self._load_resource('views')
116 |
117 | def _load_models(self):
118 | return self._load_resource('models')
119 |
120 | def _register_template_extensions(self):
121 | self.app.jinja_options = dict(Flask.jinja_options)
122 | exts = self.app.config.get('TEMPLATE_EXTENSIONS') or ['jinja2_highlight.HighlightExtension']
123 | self.app.jinja_options.setdefault('extensions',[])\
124 | .extend(exts)
125 |
126 | def _bind_extensions(self):
127 | if self.app.config.get('VERBOSE',False):
128 | print 'binding extensions'
129 | for ext_path in self.app.config.get('EXTENSIONS', []):
130 | module, e_name = self._get_imported_stuff_by_path(ext_path)
131 | if not hasattr(module, e_name):
132 | raise NoExtensionException('No {e_name} extension found'.format(e_name=e_name))
133 | ext = getattr(module, e_name)
134 | if getattr(ext, 'init_app', False):
135 | ext.init_app(self.app)
136 | else:
137 | ext(self.app)
138 |
139 | def _register_template_filters(self):
140 | if self.app.config.get('VERBOSE',False):
141 | print 'registering template filters'
142 | for filter_path in self.app.config.get('TEMPLATE_FILTERS', []):
143 | module, f_name = self._get_imported_stuff_by_path(filter_path)
144 | if hasattr(module, f_name):
145 | self.app.jinja_env.filters[f_name] = getattr(module, f_name)
146 | else:
147 | raise NoTemplateFilterException('No {f_name} template filter found'.format(f_name=f_name))
148 |
149 | def _register_context_processors(self):
150 | if self.app.config.get('VERBOSE',False):
151 | print 'registering template context processors'
152 | for processor_path in self.app.config.get('CONTEXT_PROCESSORS', []):
153 | module, p_name = self._get_imported_stuff_by_path(processor_path)
154 | if hasattr(module, p_name):
155 | self.app.context_processor(getattr(module, p_name))
156 | else:
157 | raise NoContextProcessorException('No {cp_name} context processor found'.format(cp_name=p_name))
158 |
159 | def _register_blueprints(self):
160 | if self.app.config.get('VERBOSE',False):
161 | print 'registering blueprints'
162 | self._bp = {}
163 | for blueprint_path in self.app.config.get('BLUEPRINTS', []):
164 | module, b_name = self._get_imported_stuff_by_path(blueprint_path)
165 | if hasattr(module, b_name):
166 | #self.app.register_blueprint(getattr(module, b_name))
167 | self._bp[b_name] = getattr(module,b_name)
168 | if self.app.config.get('VERBOSE',False):
169 | print 'adding {} to bp'.format(b_name)
170 | else:
171 | raise NoBlueprintException('No {bp_name} blueprint found'.format(bp_name=b_name))
172 |
173 | def _register_routes(self):
174 | if AppFactory._routes_registered is None:
175 | AppFactory._routes_registered = True
176 | if self.app.config.get('VERBOSE',False):
177 | print 'starting routing'
178 | for url_module in self.app.config.get('URL_MODULES',[]):
179 | if self.app.config.get('VERBOSE',False):
180 | pass
181 | module,r_name = self._get_imported_stuff_by_path(url_module)
182 | if self.app.config.get('VERBOSE',False):
183 | pass
184 | if r_name == 'routes' and hasattr(module,r_name):
185 | if self.app.config.get('VERBOSE',False):
186 | print '\tsetting up routing for {} with\n\troute module {}\n'.format(module.__package__,module.__name__)
187 | self._setup_routes(getattr(module,r_name))
188 | else:
189 | raise NoRouteModuleException('No {r_name} url module found'.format(r_name=r_name))
190 | if self.app.config.get('VERBOSE',False):
191 | print 'Finished registering blueprints and url routes'
192 | else:
193 | if self.app.config.get('VERBOSE',False):
194 | print 'skipped'
195 |
196 | def _check_for_registered_blueprint(self, bp):
197 | found = False
198 | for name in [str(x) for x in self.app.blueprints]:
199 | if bp.__class__.__name__.split('.')[-1] in name:
200 | found = True
201 | return found
202 |
203 | def _setup_routes(self,routes):
204 | for route in routes:
205 | blueprint,rules = route[0],route[1:]
206 | #for pattern,endpoint,view in rules:
207 | for itm in rules:
208 | if len(itm) == 3:
209 | pattern,endpoint,view = itm
210 | else:
211 | pattern,view = itm
212 | endpoint = None
213 | #for pattern,endpoint,view in rules:
214 | if self.app.config.get('VERBOSE',False):
215 | print '\t\tplugging url Pattern:',pattern
216 | print '\t\tinto View class/function:',hasattr(view,'func_name') and view.view_class.__name__ or view.__name__
217 | print '\t\tat endpoint:',endpoint or view.func_name
218 | print
219 | if type(blueprint) == type(tuple()):
220 | blueprint = blueprint[0]
221 | blueprint.add_url_rule(pattern,endpoint or view.func_name,view_func=hasattr(view,'func_name') and view or view.as_view(endpoint))
222 | if not self._check_for_registered_blueprint(blueprint):
223 | if self.app.config.get('VERBOSE',False):
224 | print '\n\t\t\tNow registering {} as blueprint\n\n'.format(str(blueprint.name))
225 | self.app.register_blueprint(blueprint)
226 |
227 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | #__Flask-XXL__
2 |
3 | -----------
4 |
5 | ###__Contents__:
6 |
7 | 1. overview
8 | 2. getting started
9 | 3. the settings file
10 | * INSTALLED_BLUEPRINTS
11 | * TEMPLATE_FILTERS
12 | * CONTEXT_PROCESSORS
13 | * ROUTE_MODULES
14 | * TEMPLATE_LOADERS
15 | * EXTRA_TEMPLATE_DIRS_
16 | 4. Flask, python and MTV
17 | 5. Views - _part1_ _begining_
18 | 6. Templates - _part1_ _begining_
19 | 7. models
20 | 8. Templates - _Advanced_
21 | 9. Widgets
22 | 10. Extending
23 | 11. Etc.....
24 |
25 | ----------------
26 |
27 | ##Ch.1 - Overview
28 |
29 | ====================
30 |
31 | Flasks creator had the idea, that Django was somewhat of an elephant,(they do say so themselves).
32 | And Who needs an elephant? Well I say there are plenty of cases to use an elephant, but why does
33 | it have to be Django? I purpose to use flask for the same monumental tasks for which we would
34 | normally look to somthing bigger. Flask by design is a small framework, it is very good at
35 | being a light, agile framework to build upon. That is why i have created Flask-XXL. Using the
36 | Flask Factory pattern, specifically one borrowed from "Flask-Kit", that uses a few settings,
37 | very similar to Django, to automatically do some things for the user, i used to find this
38 | annoying in Django, it constantly doing things behind my back. Now, i find the freedom inspiring,
39 | there is so much boilerplate i can now forget about, now i understand what everyone likes about Django.
40 | But flask is so lightweight and transparent, the source easy to read. But im rambling, so let me show
41 | you what i mean in the first example.
42 |
43 |
44 |
45 | ##Ch.2 - Getting Started
46 |
47 | ============================
48 |
49 | a typical small flask app might start like this
50 |
51 | from flask import Flask,url_for,redirect,session,g,flash
52 |
53 | app = Flask(__name__)
54 |
55 | @app.route('/')
56 | def index():
57 | return "hello World"
58 |
59 | if __name__ == "__main__":
60 | app.run()
61 |
62 |
63 | but this is obviously a very small example. If you really want a system
64 | that can expand you should start using Blueprints
65 |
66 | from flask import Blueprint
67 |
68 | blog = Blueprint('blog',__name__,
69 | template_folder='templates',
70 | url_prefix='/blog')
71 | from .views import *
72 |
73 | now most tutorials say to keep going like the original
74 | example
75 |
76 | @blog.route('/')
77 | def blog():
78 | return "my blog"
79 |
80 |
81 | but this is where we take our _turn_,
82 |
83 | so the first thing to look at in any big web system like this, id say is the settings file, or whichever
84 | config holds the database info.
85 |
86 | here thats
settings.py a file that is very similar now to djangos
settings.py
87 |
88 |
89 |
90 |
91 | ##Ch.3 Settings file
92 |
93 | ======================
94 |
95 | the main settings here are
96 |
97 | #Django Equivilants
98 | #++++++++++++++++++++
99 | INSTALLED_BLUEPRINTS = [ #<-- INSTALLED_APPS
100 | XXXXX
101 | ]
102 | TEMPLATE_FILTERS = [ [ #<-- Django dosent have a setting for this feature, just a clunky api
103 | XXXXXX
104 | ]
105 | CONTEXT_PROCESSORS = [ #<-- TEMPLATE_CONTEXT_PROCESSORS
106 | XXXXXXX
107 | ]
108 | ROUTE_MODULES = [ #<-- ROOT_URLCONF
109 | XXXXXX
110 | ]
111 | TEMPLATE_LOADERS = [ #<-- TEMPLATE_LOADERS
112 | xxxxxx
113 | ]
114 | EXTRA_TEMPLATE_DIRS = [ #<-- TEMPLATE_DIRS
115 | xxxxxxxx
116 | ]
117 |
118 |
119 | These 4 settings handle many things that normally take quite some time to get right,
120 | and boils them down to a few easy processes.
121 |
122 | ###INSTALLED_BLUEPRINTS
123 |
124 | +++++++++++++++++++++++
125 |
126 | this takes the place of Djangos
INSTALLED_APPS but for flask blueprints.
127 | It works the same way. just add the import path of a Blueprint, it can be anywhere,
128 | even in a seperate installed package, as long as its importable on your PYTHONPATH.
129 | Heres a an example of the setting
130 |
131 | INSTALLED_BLUEPRINTS = [
132 | 'flask.ext.xxl.apps.auth.auth', #these are a few of the blueprints
133 | 'flask.ext.xxl.apps.page.page', #built into flask-xxl
134 | 'admin.admin', # these are blueprints inside the webapp
135 | 'blog.blog'
136 | ]
137 |
138 | ###TEMPLATE_FILTERS
139 |
140 | +++++++++++++++++++
141 |
142 | now in Django you can write and register your own template filters,
143 | but i think its kind of a hassle. Flask has an easy way to do it that ive always
144 | loved, but this makes it much eaiser.
145 |
146 | 1. step1 - write the filter heres an example that will pull all of the x's from the input and return the result
147 |
148 |
149 |
150 | def filter_out_xs(data):
151 | rtn = ''
152 | for char in data:
153 | if char.lower() == 'x':
154 | pass
155 | else:
156 | rtn += char
157 | return rtn
158 |
159 |
160 | 2. step2 just add to the setting, if it was in myapp.template_filters i would put
161 |
162 |
163 |
164 | TEMPLATE_FILTERS = [
165 | 'myapp.template_filters.filter_out_xs',
166 | ]
167 |
168 |
169 | then it could be used in a template like this:
170 |
171 | {{ someval|filter_out_xs }}
172 |
173 | and of the input was
174 | > 'xx55x76'
175 |
176 | the output would be
177 | > '5576'
178 |
179 |
180 | ###CONTEXT_PROCESSORS
181 |
182 | +++++++++++++++++++++
183 |
184 |
185 | A better way to think of context_processors, is global template variables.
186 | Basicly they are functions you can define, that can return data and objects
187 | into the global template environment. Then those objects or that data, is avialiable
188 | upon rendering templates. This can be very powerful, and it is just as easy here as it is
189 | in Django.
190 |
191 | Heres a simple example:
192 |
193 | def simple_context():
194 | return dict(somevar='someval')
195 |
196 | def extra_func():
197 | return 'somthing i computed'
198 |
199 | def complicated_context():
200 | return dict(complicated=extra_func)
201 |
202 |
203 | now this is in myapp/context_processors.py in the settings file id put
204 |
205 | CONTEXT_PROCESSORS = [
206 | 'myapp.context_processors.simple_context',
207 | 'myapp.context_processors.complicated_context',
208 | ]
209 |
210 | then in my templates
somevar and
extra_func would be avialible
211 |
212 |
213 |
214 |
215 |
216 | ##Ch. 4 Flask, Python.. and MTV
217 |
218 | --------------------------------
219 |
220 |
221 | Now most people, well at least most programmers, know about the MVC or Model, View, Controller pattern. In
222 | Python things tend to work a little differently, first lets look at the traditional __MVC__ pattern.
223 |
224 | * a model class mapping database tables to objects and associcated data within the program
225 | * M
226 | * a few view classes (with related latout,block or template files/classes) to process that data for display
227 | * V
228 | * finally You would normally have a some kind of front controller class that routes http requests to the views
229 | * C
230 |
231 |
232 | now python dosent handle the http requests as cleanly as some languages might, and as a consiquense of that
233 | the routing mainly happens behind the scenes, and is handeled on by the user via either a config style syntax,
234 | or a decoraotor syntax ie:
235 |
236 | config style:
237 |
238 | ```python
239 | routes = (
240 | ((Bp),
241 | ('/',AView.as_view('view')),
242 | )
243 | ```
244 |
245 | decorator style:
246 | ```python
247 | @app.route('/')
248 | def view_func():
249 | return a_view()
250 | ```
251 | and this leads us to our next setting
252 |
253 | ###ROUTE_MODULES
254 |
255 | ++++++++++++++++
256 |
257 | now flask and its many tutorials teach the decorator style from above, arguing that it makes debugging
258 | a crashed site eaiser because the function that crashed the site is defined right next to the url that called it.
259 | Which makes sense, until your project grows to 500+ lines, then those functions start getting harder to sift through. Not to mention that the decorator syntax makes it easy to assign different request methods to the same functions, then checking in the function which type of request was sent and basing the response on that. This to me seems like a lot for nothing. The great thing about it though is it uses simple string matching to line up incoming requests with the defined url rules, this isnt as powerful as some systems, but it is simple to understand and you can get up and running in a short matter of time.
260 |
261 | Django on the other hand uses the config style, not exactly like my example above, but close enough. In the Django world each app you add to your project you give a urls.py file that imports your view functions/classes and matchs them to url rules. I find this makes debugging actually eaiser. Anytime your site crashes, the url will tell you which urls.py file to look in, then just find that line and theres your offending function. The main downside with Django is the it uses a complex system of _"regular expressions"_ to parse and match the incoming url requests to the defined url rules. I must admit that no matter how trivial they may sound, regular expressions are anything but.
262 |
263 | So for my system i took the best of both of those worlds.
264 | * a _urls.py_ file
265 | * each app (blueprint in flask speak) gets its own urls.py for routing
266 |
267 | * string comparisons for matching requests to url rules
268 |
269 |
270 | Here is a _"small"_ example to show you how a view and its routing are defined:
271 | (dont mind the flask-xxl imports, ill explain them later)
272 |
273 | views.py
274 | ```python
275 | from flask.ext.xxl.core.baseviews import BaseView
276 |
277 | class ExampleView(BaseView):
278 | _template = 'example.html'
279 |
280 | def get(self):
281 | return self.render()
282 | ```
283 |
284 | now we need a blueprint to plug it into
285 |
286 | example.py
287 | ```python
288 | from flask import Blueprint
289 |
290 | example = Blueprint('example',__file__)
291 | from .views import *
292 | ```
293 |
294 | then we need to define our route
295 |
296 | urls.py
297 | ```python
298 | from .views import ExampleView
299 | from example import eaxmple
300 |
301 | routes = [
302 | ((example),
303 | ('/',ExampleView.as_view('example')),
304 | )]
305 | ```
306 |
307 | then just define the url module in the
ROUTE_MODULE setting
308 |
309 | settings.py
310 | ```python
311 |
312 | ROUTE_MODULES = (
313 | 'myapp.example.urls',
314 | )
315 | ```
316 |
317 |
318 |
319 | now i know this may seem like a bit much for such a small example app,
320 | but lets not forget that this is called _Flask-XXL_, so its really not intended
321 | for such small requirments, if you plan on having a small web-app, with few blueprints,
322 | that is what Flask is made for, but when you plan on your application growing
323 | and you want to handle it with clean system, that is what flask-xxl is for.
324 |
325 |
326 | ##Ch. 5. Views - _part1_ _begining_
327 |
328 | -----------
329 | from here on out we will dump our example app and start fresh, building what hopefully
330 | will grow to be a full-featured project management suite. But we will start from the start, or
331 | at least as far back as you need to when using flask-xxl.
332 |
333 | Now one thing i think almost everyone can agree Django did well was project / (module/app) setup.
334 |
335 | with a simple
336 |
337 | ```bash
338 |
339 | $ manage.py startproject
340 |
341 | ```
342 | or
343 | ```bash
344 |
345 | $ manage.py startapp
346 | ```
347 | you are asked a few simple questions about your intentions and
348 | wham boom, youve got a new project/app, or at least a number of the files youll
349 | want to start off with, as well as a few helpful imports / commented notes to
350 | help speed up development.
351 |
352 | Thats the way things should work, so here to start we use
353 |
354 | ```bash
355 |
356 | $ flaskxxl.py startproject
357 |
358 | ```
359 |
360 |
361 |
362 |
363 | ##To Be Continued __.....__
364 |
365 | ------------------
366 |
--------------------------------------------------------------------------------
/flask_xxl/apps/admin/templates/_macros.html:
--------------------------------------------------------------------------------
1 | {#
2 | render_nav_dropdown()
3 | @args:
4 | label(str)
5 | link_list(list)
6 | @notes:
7 | link_list s/b list of either (text,endpoint) or ('sep',None) tuples
8 | #}
9 | {% macro render_nav_dropdown(label,link_list) %}
10 |
{{ label }}
11 |
22 | {% endmacro %}
23 |
24 | {#
25 | render_nav_link()
26 | @args:
27 | label(str)
28 | endpoint(str)
29 | @notes:
30 | highlights link with given endpoint
31 | #}
32 | {% macro render_nav_link(label,endpoint) %}
33 |
{{ endpoint }}
34 | {% endmacro %}
35 |
36 | {#
37 | render_navbar()
38 | @args:
39 | brand(str)
40 | nav_links(list)
41 | dropdowns(list)
42 | inverse(bool) - False
43 | auth(bool) - False
44 | @notes:
45 | create navbar (mainly for admin interface)
46 | #}
47 | {% macro render_navbar(brand,nav_links,dropdowns,inverse=false,auth=false) %}
48 | {% if inverse %}
49 | {% set type = 'inverse' %}
50 | {% else %}
51 | {% set type = 'default' %}
52 | {% endif %}
53 |
54 |
55 |
63 |
64 |
65 | {% for endpoint,txt in nav_links %}
66 | {{ render_nav_link(txt,endpoint) }}
67 | {% endfor %}
68 | {% if dropdowns %}
69 | {% for dropdown in dropdowns %}
70 | {% for label,link_list in dropdown.items() %}
71 |
72 | {{ render_nav_dropdown(label,link_list) }}
73 |
74 | {% endfor %}
75 | {% endfor %}
76 | {% endif %}
77 |
78 | {% if auth %}
79 |
84 | {% endif %}
85 |
86 |
87 |
88 | {% endmacro %}
89 |
90 | {#
91 | render_bs3_field()
92 | @args:
93 | field(wtforms.Field)
94 | label(bool) - True
95 |
96 | @notes:
97 | render field with bs3 styles
98 | #}
99 | {% macro render_field(field,label=true) %}
100 |
111 | {% endmacro %}
112 |
113 | {#
114 | render_checkbox()
115 | @args:
116 | field(wtforms.Field)
117 | @notes:
118 | renders bs3 compliant checkbox
119 | #}
120 | {% macro render_checkbox(field) %}
121 |
122 |
123 | {{ field(type='checkbox',**kwargs) }} {{ field.label }}
124 |
125 |
126 | {% endmacro %}
127 |
128 | {#
129 | render_radio()
130 | @args:
131 | field(wtforms.Field)
132 | @notes:
133 | renders bs3 compliant radio select
134 | #}
135 | {% macro render_radio(field) %}
136 | {% for value,label,_ in field.iter_choices() %}
137 |
138 |
139 | {{ label }}
140 |
141 |
142 | {% endfor %}
143 | {% endmacro %}
144 |
145 | {% macro render_content_link(field) %}
146 | {% if 'page' in request.url %}
147 | {% if 'edit' in request.url %}
148 | {% set link_url = 'admin.edit_page_content' %}
149 | {% else %}
150 | {% set link_url = 'admin.page_content' %}
151 | {% endif %}
152 | {% elif 'block' in request.url %}
153 | {% if 'edit' in request.url %}
154 | {% set link_url = 'admin.edit_block_content' %}
155 | {% else %}
156 | {% set link_url = 'admin.block_content' %}
157 | {% endif %}
158 | {% endif %}
159 |
Edit Content
161 | {% endmacro %}
162 |
163 |
164 | {% macro render_form(form,action='',action_text='Submit',class_='',btn_class='btn btn-default') %}
165 |
243 | {% endmacro %}
244 |
245 | {% macro render_panel_form(form,heading='',action='',action_text='Submit',class_='form',btn_class='btn btn-default') %}
246 |
247 |
259 |
260 | {% endmacro %}
261 |
262 | {% macro render_dual_panel_form(form_one_args,form_two_args) %}
263 | {# form_args should be (form, heading='',action='',action_text='Submit',class_='form',btn_class='btn btn-default') #}
264 |
265 |
266 |
278 |
279 | {% endmacro %}
280 |
281 | {% macro render_pagination(pag) %}
282 |
286 | {% endmacro %}
287 |
288 |
289 | {% macro render_centered_form(form,heading='',action='',action_text='Submit',class_='form',btn_class='btn btn-default') %}
290 |
304 | {% endmacro %}
305 |
306 | {% macro render_nav_sidebar(link_list,title) %}
307 |
317 | {% endmacro %}
318 |
319 |
320 |
--------------------------------------------------------------------------------
/flask_xxl/apps/page/templates/new_post.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin.html' %}
2 | {% block extra_head %}
3 | {% if codemirror %}
4 | {{ codemirror.include_codemirror() }}
5 | {% endif %}
6 |
16 | {% endblock extra_head %}
17 | {% block body %}
18 |
19 | {{ lipsum() }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
180 |
181 |
188 |
189 |
190 |
191 |
192 |
210 |
211 | {% if form.description %}
212 | {{ form.description(class_="form-control",placeholder="Description") }}
213 | {% else %}
214 |
215 | {% endif %}
216 |
217 |
218 |
219 |
220 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 | {% endblock body %}
248 | {% block footer_js %}
249 | {{ super() }}
250 |
290 | {% endblock footer_js %}
291 |
292 |
--------------------------------------------------------------------------------