├── .gitignore ├── LICENSE ├── README.md ├── README_zh.md ├── app.py ├── content ├── config.py └── users.json ├── form.py ├── model.py ├── requirements.txt ├── static ├── ajaxfileupload.js ├── bootstrap.css ├── bootstrap.min.js ├── favicon.ico ├── jquery.min.js ├── jsterm │ ├── command.js │ ├── config.js │ ├── font-awesome.min.css │ ├── jack.png │ ├── jsterm.js │ ├── sample.js │ └── style.css ├── md.css ├── pygments.css ├── responsive.css └── style.css ├── templates ├── 404.html ├── about.html ├── addLnk.html ├── base.html ├── create.html ├── editor.html ├── helpers.html ├── home.html ├── index.html ├── login.html ├── move.html ├── page.html ├── search.html ├── tag.html ├── tags.html └── upload.html └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *.md 5 | upload 6 | nohup.out 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | 58 | content/* 59 | static/upload 60 | uploads.json 61 | 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 jack.zh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zwiki 2 | ===== 3 | 4 | ## version 5 | 6 | 0.3.0 7 | 8 | + [中文](https://github.com/jack-zh/zwiki/blob/master/README_zh.md) 9 | 10 | A simple wiki(blog?) written in Flask. 11 | 12 | ## Start 13 | 14 | git clone https://github.com/jack-zh/zwiki.git 15 | cd zwiki 16 | pip install -r requirements.txt 17 | gunicorn app:app 18 | 19 | ### About config`/static/jsterm/config.js` 20 | 21 | ## Next version 22 | + The attachment list 23 | + The custom Menu 24 | 25 | ## Note 26 | 27 | You can edit `static/jsterm/config.js`, set yourself message for geek menu 28 | 29 | ## LICENSE 30 | 31 | MIT 32 | 33 | ## Example online 34 | 35 | [http://demo.zwiki.link-pub.cn](http://demo.zwiki.link-pub.cn) 36 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # zwiki 2 | 3 | ##### 以wiki的名义构建的简易易扩展的个人记事本(Blog?) 4 | ##### 学习Flask的成果 5 | ===== 6 | 7 | + [中文](https://github.com/jack-zh/zwiki/blob/master/README_zh.md) 8 | + [English](https://github.com/jack-zh/zwiki/blob/master/README.md) 9 | 10 | ## version 11 | 12 | 0.3.0 13 | 14 | ## 开始 15 | 16 | #### 1. 获取代码 17 | 18 | git clone https://github.com/jack-zh/zwiki.git 19 | 20 | #### 2. 安装zwiki 21 | 22 | cd zwiki 23 | pip install -r requirements.txt 24 | 25 | #### 3. 运行zwiki 26 | 27 | gunicorn app:app 28 | 29 | #### 4. 打开浏览器 30 | 地址:http://localhost:8000/ 31 | 32 | #### 5. 页面说明 33 | + `zWiki`是页面的主Title 34 | + `Home`是主页面,在程序开始的时候设置 35 | + `Index`页面是所有的wiki列表 36 | + `Tags`是标签列表 37 | + `Search`是全局搜索入口 38 | + `New Page` 新添加页面配置 39 | 40 | ### 6. About的配置页面在`/static/jsterm/config.js` 41 | 42 | ## 设置 43 | 44 | #### 启动设定端口和IP: 45 | 46 | gunicorn -b ip:port app:app 47 | 48 | 比如: 49 | 50 | gunicorn -b 8.8.8.8:6789 app:app 51 | 52 | 当然,你可以后台运行它 53 | 54 | nohup gunicorn -b 8.8.8.8:6789 app:app & 55 | 56 | 57 | #### config.py配置说明: 58 | 59 | `content/config.py`是一个全局配置文件,程序启动的时候优先寻找`content/user_config.py`文件,当查找不到得时候会加载`config.py`。即我们可以配置自己的`user_config.py`,也可以在`confif.py`的基础上更改。 60 | 61 | # encoding: utf-8 62 | 63 | SECRET_KEY='JACK_ZH' # session key 64 | TITLE='zWiki' # wiki title 65 | 66 | CONTENT_DIR="markdown" # wiki(blog) save file dir 67 | USER_CONFIG_DIR="content" # ... 68 | PRIVATE=False # logout edit del ... flag 69 | SHOWPRIVATE=False # logout show flag 70 | UPLOAD_DIR="./static/upload" 71 | 72 | # from 畅言: http://changyan.sohu.com/install/code/pc 73 | SOHUCS_APPID = "cyrE7gU83" 74 | SOHUCS_CONF = "prod_1f3b1e3a86d5da44e0295ab22fb27033" 75 | 76 | + `SECRET_KEY` 一个session key字符串,建议你在部署你的`wiki`时生成自己的key 77 | + `TITLE='zWiki'` 标题,更改成你要现实的文字, 比如`Jack'Blog` 78 | + `CONTENT_DIR="markdown"` 我们添加的`md`文件的保存路径,此配置的意思是路径在此目录下的"markdown"文件夹内 79 | + `USER_CONFIG_DIR="content"` 配置文件加载路径 建议不加更改 80 | + `PRIVATE=False` 当更改我们的wiki时是否需要验证 81 | + `SHOWPRIVATE=False` 当查看我们的wiki时,是否需要验证 82 | + `SOHUCS_APPID = "cyrE7gU83"` 畅言的注册后的appid 83 | + `SOHUCS_CONF = "prod_1f3b1e3a86d5da44e0295ab22fb27033"` 畅言注册后的conf 84 | 85 | #### users.py配置说明: 86 | 87 | `content/users.py`是一个全局配置文件,程序启动的时候优先寻找`content/user_users.py`文件,当查找不到得时候会加载`users.py`。即我们可以配置自己的`user_users.py`,也可以在`users.py`的基础上更改。这个配置文件是用来配置登录信息的。可以支持多用户。 88 | 89 | { 90 | "jack": { 91 | "active": true, 92 | "authentication_method": "cleartext", 93 | "password": "123456", 94 | "authenticated": false, 95 | "roles": "admin" 96 | } 97 | } 98 | 99 | #### About 信息配置 100 | 101 | 位置:/static/jsterm/config.js 102 | 103 | var CONFIG = CONFIG || { 104 | python_version: 'Python 2.7.9', 105 | gcc_version: '4.8.2', 106 | sys_platform: 'linux2', 107 | realname: '张志贺', 108 | username: 'jack', 109 | email: 'zzh.coder@qq.com', 110 | weibo: 'http://weibo.com/zzhcoder', 111 | github: 'http://github.com/jack-zh', 112 | blog: 'http://link-pub.cn', 113 | first: true 114 | }; 115 | ##### About的ls gimp cat配置 116 | 117 | 位置:/static/jsterm/sample.js 118 | 119 | { 120 | "name": "~", 121 | "type": "dir", 122 | "contents": [ 123 | { 124 | "name": "jack", 125 | "type": "img", 126 | "contents": "/static/jsterm/jack.png", 127 | "caption": "Yes! me!" 128 | }, 129 | { 130 | "name": "Technology", 131 | "type": "dir", 132 | "contents": [ 133 | { 134 | "name": "Python", 135 | "type": "text", 136 | "contents": "Tornado, Flask, PyQt, 科学计算" 137 | }, 138 | { 139 | "name": "NodeJS", 140 | "type": "text", 141 | "contents": "NodeJS express plus plus..." 142 | }, 143 | { 144 | "name": "Linux-C", 145 | "type": "text", 146 | "contents": "Linux-C开发,网络开发,PowerPC,驱动开发" 147 | }, 148 | { 149 | "name": "Golang", 150 | "type": "text", 151 | "contents": "Golang深度学习者" 152 | }, 153 | { 154 | "name": "FrontEnd", 155 | "type": "text", 156 | "contents": "初学者,水深,设计" 157 | }, 158 | { 159 | "name": "OPS", 160 | "type": "dir", 161 | "contents": [ 162 | { 163 | "name": "Docker", 164 | "type": "text", 165 | "contents": "Docker" 166 | }, 167 | { 168 | "name": "OpenStack", 169 | "type": "text", 170 | "contents": "OpenStack" 171 | } 172 | ] 173 | } 174 | ] 175 | }, 176 | { 177 | "name": "Live", 178 | "type": "dir", 179 | "contents": [ 180 | { 181 | "name": "Self", 182 | "type": "text", 183 | "contents": "身高:180 cm\n体重:80 kg\n职业:程序员\n爱好:篮球\n运动:骑行\n居住:武汉\n籍贯:内蒙古\n民族:蒙古族" 184 | }, 185 | { 186 | "name": "Family", 187 | "type": "dir", 188 | "contents": [ 189 | { 190 | "name": "Daughter", 191 | "type": "text", 192 | "contents": "我爱你,么么哒" 193 | }, 194 | { 195 | "name": "Mother", 196 | "type": "text", 197 | "contents": "我爱你,么么哒" 198 | }, 199 | { 200 | "name": "Wife", 201 | "type": "text", 202 | "contents": "我爱你,么么哒" 203 | } 204 | ] 205 | } 206 | ] 207 | } 208 | ] 209 | } 210 | 211 | 212 | + `"jack"` 用户名 213 | + `password` 密码 214 | + 其他字段 保留字段,暂时不需要更改 215 | 216 | ## LICENSE 217 | 218 | MIT 219 | 220 | ## Example online 221 | 222 | [http://demo.zwiki.link-pub.cn](http://demo.zwiki.link-pub.cn) 223 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import json 4 | from functools import wraps 5 | from flask import (Flask, render_template, flash, redirect, url_for, request) 6 | from werkzeug.utils import secure_filename 7 | from flask.ext.login import (LoginManager, login_required, current_user, login_user, logout_user) 8 | from flask.ext.script import Manager 9 | 10 | from model import Wiki, UserManager 11 | from form import URLForm, SearchForm, EditorForm, LoginForm, AddLnkForm, Processors 12 | from utils import make_salted_hash, check_hashed_password, allowed_file, get_save_name, get_md5, save_uploadfile_to_backup 13 | 14 | 15 | import sys 16 | reload(sys) 17 | sys.setdefaultencoding('utf-8') 18 | 19 | 20 | def get_default_authentication_method(): 21 | return app.config.get('DEFAULT_AUTHENTICATION_METHOD', 'cleartext') 22 | 23 | 24 | def protect(f): 25 | @wraps(f) 26 | def wrapper(*args, **kwargs): 27 | if app.config.get('PRIVATE') and not current_user.is_authenticated: 28 | return loginmanager.unauthorized() 29 | return f(*args, **kwargs) 30 | return wrapper 31 | 32 | 33 | def showprotect(f): 34 | @wraps(f) 35 | def wrapper(*args, **kwargs): 36 | if app.config.get('SHOWPRIVATE') and not current_user.is_authenticated: 37 | return loginmanager.unauthorized() 38 | return f(*args, **kwargs) 39 | return wrapper 40 | 41 | 42 | app = Flask(__name__) 43 | 44 | if os.path.exists("content/user_config.py"): 45 | config_filename = "user_config.py" 46 | elif os.path.exists("content/config.py"): 47 | config_filename = "config.py" 48 | else: 49 | print ("Startup Failure: You need to place a " 50 | "config.py or user_config.py in your content directory.") 51 | exit(1) 52 | 53 | app.config['CONTENT_DIR'] = 'content' 54 | app.config['TITLE'] = 'wiki' 55 | 56 | app.config['TITLELNK'] = [] 57 | 58 | app.config.from_pyfile( 59 | os.path.join(app.config.get('CONTENT_DIR'), config_filename) 60 | ) 61 | 62 | manager = Manager(app) 63 | 64 | loginmanager = LoginManager() 65 | loginmanager.init_app(app) 66 | loginmanager.login_view = 'user_login' 67 | 68 | wiki = Wiki(app.config.get('CONTENT_DIR')) 69 | 70 | users = UserManager(app.config.get('USER_CONFIG_DIR')) 71 | 72 | if not os.path.exists(app.config.get('UPLOAD_DIR')): 73 | os.makedirs(app.config.get('UPLOAD_DIR')) 74 | 75 | 76 | @loginmanager.user_loader 77 | def load_user(name): 78 | return users.get_user(name) 79 | 80 | 81 | @app.route('/') 82 | @showprotect 83 | def home(): 84 | page = wiki.get('home') 85 | if page: 86 | return display('home') 87 | return render_template('home.html') 88 | 89 | 90 | @app.route('/index/') 91 | @showprotect 92 | def index(): 93 | pages = wiki.index() 94 | return render_template('index.html', pages=pages) 95 | 96 | 97 | @app.route('//') 98 | @showprotect 99 | def display(url): 100 | page = wiki.get_or_404(url) 101 | return render_template('page.html', page=page) 102 | 103 | 104 | @app.route('/create/', methods=['GET', 'POST']) 105 | @protect 106 | def create(): 107 | form = URLForm() 108 | if form.validate_on_submit(): 109 | return redirect(url_for('edit', url=form.clean_url(form.url.data))) 110 | return render_template('create.html', form=form) 111 | 112 | 113 | @app.route('/edit//', methods=['GET', 'POST']) 114 | @protect 115 | def edit(url): 116 | page = wiki.get(url) 117 | form = EditorForm(obj=page) 118 | if form.validate_on_submit(): 119 | if not page: 120 | page = wiki.get_bare(url) 121 | form.populate_obj(page) 122 | page.save() 123 | flash('"%s" was saved.' % page.title, 'success') 124 | return redirect(url_for('display', url=url)) 125 | return render_template('editor.html', form=form, page=page) 126 | 127 | 128 | @app.route('/preview/', methods=['POST']) 129 | @showprotect 130 | def preview(): 131 | a = request.form 132 | data = {} 133 | processed = Processors(a['body']) 134 | data['html'], data['body'], data['meta'] = processed.out() 135 | return data['html'] 136 | 137 | 138 | @app.route('/move//', methods=['GET', 'POST']) 139 | @protect 140 | def move(url): 141 | page = wiki.get_or_404(url) 142 | form = URLForm(obj=page) 143 | if form.validate_on_submit(): 144 | newurl = form.url.data 145 | wiki.move(url, newurl) 146 | return redirect(url_for('.display', url=newurl)) 147 | return render_template('move.html', form=form, page=page) 148 | 149 | 150 | @app.route('/delete//') 151 | @protect 152 | def delete(url): 153 | page = wiki.get_or_404(url) 154 | wiki.delete(url) 155 | flash('Page "%s" was deleted.' % page.title, 'success') 156 | return redirect(url_for('home')) 157 | 158 | 159 | @app.route('/tags/') 160 | @showprotect 161 | def tags(): 162 | tags = wiki.get_tags() 163 | return render_template('tags.html', tags=tags) 164 | 165 | 166 | @app.route('/tag//') 167 | @showprotect 168 | def tag(name): 169 | tagged = wiki.index_by_tag(name) 170 | return render_template('tag.html', pages=tagged, tag=name) 171 | 172 | 173 | @app.route('/search/', methods=['GET', 'POST']) 174 | @showprotect 175 | def search(): 176 | form = SearchForm() 177 | if form.validate_on_submit(): 178 | results = wiki.search(form.term.data) 179 | return render_template('search.html', form=form, 180 | results=results, search=form.term.data) 181 | return render_template('search.html', form=form, search=None) 182 | 183 | 184 | @app.route('/user/login/', methods=['GET', 'POST']) 185 | def user_login(): 186 | form = LoginForm() 187 | if form.validate_on_submit(): 188 | user = users.get_user(form.name.data) 189 | login_user(user) 190 | user.set('authenticated', True) 191 | flash('Login successful.', 'success') 192 | return redirect(request.args.get("next") or url_for('index')) 193 | return render_template('login.html', form=form) 194 | 195 | 196 | @app.route('/user/logout/') 197 | @login_required 198 | def user_logout(): 199 | current_user.set('authenticated', False) 200 | logout_user() 201 | flash('Logout successful.', 'success') 202 | return redirect(url_for('index')) 203 | 204 | 205 | @app.route('/about/') 206 | @showprotect 207 | def about(): 208 | return render_template('about.html') 209 | 210 | 211 | @app.route('/upload/', methods=['GET']) 212 | @showprotect 213 | def show_upload(): 214 | tags = wiki.get_tags() 215 | if os.path.exists(os.path.join(app.config.get('CONTENT_DIR'), "uploads.json")): 216 | fd = open(os.path.join(app.config.get('CONTENT_DIR'), "uploads.json"), "r") 217 | _s = fd.read() 218 | fd.close() 219 | uploads = json.loads(_s) 220 | else: 221 | uploads = {} 222 | return render_template('upload.html', uploads=uploads) 223 | 224 | 225 | @app.route('/upload/', methods=['POST']) 226 | @protect 227 | def post_upload(): 228 | file = request.files['file'] 229 | if file and allowed_file(file.filename): 230 | filename = secure_filename(file.filename) 231 | savename = get_save_name(filename) 232 | filepath = os.path.join(app.config.get('UPLOAD_DIR'), savename) 233 | file.save(filepath) 234 | staticfilepath = filepath[1:].replace("\\", "/") 235 | bobj = {"filename":filename, "url":staticfilepath, "error": False} 236 | 237 | if os.path.exists(os.path.join(app.config.get('CONTENT_DIR'), "uploads.json")): 238 | fd = open(os.path.join(app.config.get('CONTENT_DIR'), "uploads.json"), "r") 239 | _s = fd.read() 240 | fd.close() 241 | _os = json.loads(_s) 242 | else: 243 | _os = {} 244 | 245 | _os[savename] = filename 246 | _s = json.dumps(_os) 247 | fd = open(os.path.join(app.config.get('CONTENT_DIR'), "uploads.json"), "w") 248 | fd.write(_s) 249 | fd.close() 250 | 251 | else: 252 | bobj = {'error':True} 253 | 254 | if not bobj['error']: 255 | save_uploadfile_to_backup(filepath) 256 | return json.dumps(bobj) 257 | 258 | 259 | @app.route('/addlnk/', methods=['GET', 'POST']) 260 | @protect 261 | def addlnk(): 262 | form = AddLnkForm() 263 | if form.validate_on_submit(): 264 | url = form.clean_url(form.url.data) 265 | app.config['TITLELNK'].append({"title":form.title.data, "url":url}) 266 | return redirect(url_for('edit', url=url)) 267 | return render_template('addLnk.html', form=form) 268 | 269 | 270 | @app.route('/user/') 271 | @protect 272 | def user_index(): 273 | pass 274 | 275 | 276 | @app.route('/user/create/') 277 | @protect 278 | def user_create(): 279 | pass 280 | 281 | 282 | @app.route('/user//') 283 | @protect 284 | def user_admin(user_id): 285 | pass 286 | 287 | 288 | @app.route('/user/delete//') 289 | @protect 290 | def user_delete(user_id): 291 | pass 292 | 293 | 294 | @app.errorhandler(404) 295 | def page_not_found(e): 296 | return render_template('404.html'), 404 297 | 298 | 299 | if __name__ == '__main__': 300 | manager.run() 301 | -------------------------------------------------------------------------------- /content/config.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | SECRET_KEY='JACK_ZH' # session key 4 | TITLE='zWiki' # wiki title 5 | 6 | CONTENT_DIR="markdown" # wiki(blog) save file dir 7 | USER_CONFIG_DIR="content" # ... 8 | PRIVATE=False # logout edit del ... flag 9 | SHOWPRIVATE=False # logout show flag 10 | UPLOAD_DIR="./static/upload" 11 | 12 | # from 畅言: http://changyan.sohu.com/install/code/pc 13 | SOHUCS_APPID = "cyrE7gU83" 14 | SOHUCS_CONF = "prod_1f3b1e3a86d5da44e0295ab22fb27033" -------------------------------------------------------------------------------- /content/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "jack": { 3 | "active": true, 4 | "authentication_method": "cleartext", 5 | "password": "123456", 6 | "authenticated": false, 7 | "roles": "admin" 8 | } 9 | } -------------------------------------------------------------------------------- /form.py: -------------------------------------------------------------------------------- 1 | from flask.ext.wtf import Form 2 | from wtforms import (TextField, TextAreaField, PasswordField) 3 | from wtforms.validators import (InputRequired, ValidationError) 4 | 5 | from model import Processors 6 | 7 | 8 | class URLForm(Form): 9 | url = TextField('', [InputRequired()]) 10 | 11 | def validate_url(form, field): 12 | from app import wiki 13 | if wiki.exists(field.data): 14 | raise ValidationError('The URL "%s" exists already.' % field.data) 15 | 16 | def clean_url(self, url): 17 | return Processors().clean_url(url) 18 | 19 | 20 | class SearchForm(Form): 21 | term = TextField('', [InputRequired()]) 22 | 23 | 24 | class EditorForm(Form): 25 | title = TextField('', [InputRequired()]) 26 | body = TextAreaField('', [InputRequired()]) 27 | tags = TextField('') 28 | 29 | 30 | class AddLnkForm(Form): 31 | title = TextField('', [InputRequired()]) 32 | url = TextField('', [InputRequired()]) 33 | 34 | def validate_url(form, field): 35 | from app import wiki 36 | if wiki.exists(field.data): 37 | raise ValidationError('The URL "%s" exists already.' % field.data) 38 | 39 | def clean_url(self, url): 40 | return Processors().clean_url(url) 41 | 42 | class LoginForm(Form): 43 | name = TextField('', [InputRequired()]) 44 | password = PasswordField('', [InputRequired()]) 45 | 46 | def validate_name(form, field): 47 | from app import users 48 | user = users.get_user(field.data) 49 | if not user: 50 | raise ValidationError('This username does not exist.') 51 | 52 | def validate_password(form, field): 53 | from app import users 54 | user = users.get_user(form.name.data) 55 | if not user: 56 | return 57 | if not user.check_password(field.data): 58 | raise ValidationError('Username and password do not match.') 59 | 60 | -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import re 4 | import markdown 5 | import json 6 | from flask import (url_for ,abort) 7 | 8 | 9 | class Processors(object): 10 | def __init__(self, content=""): 11 | self.content = self.pre(content) 12 | 13 | def wikilink(self, html): 14 | link = r"((?)\[\[([^<].+?) \s*([|] \s* (.+?) \s*)?]])" 15 | compLink = re.compile(link, re.X | re.U) 16 | for i in compLink.findall(html): 17 | title = [i[-1] if i[-1] else i[1]][0] 18 | url = self.clean_url(i[1]) 19 | formattedLink = u"{1}".format(url_for('display', url=url), title) 20 | html = re.sub(compLink, formattedLink, html, count=1) 21 | return html 22 | 23 | def clean_url(self, url): 24 | pageStub = re.sub('[ ]{2,}', ' ', url).strip() 25 | pageStub = pageStub.lower().replace(' ', '_') 26 | pageStub = pageStub.replace('\\\\', '/').replace('\\', '/') 27 | return pageStub 28 | 29 | def pre(self, content): 30 | return content 31 | 32 | def post(self, html): 33 | return self.wikilink(html) 34 | 35 | def out(self): 36 | md = markdown.Markdown(['codehilite', 'fenced_code', 'meta', 'tables']) 37 | html = md.convert(self.content) 38 | phtml = self.post(html) 39 | body = self.content.split('\n\n', 1)[1] 40 | meta = md.Meta 41 | return phtml, body, meta 42 | 43 | 44 | class Page(object): 45 | def __init__(self, path, url, new=False): 46 | self.path = path 47 | self.url = url 48 | self._meta = {} 49 | if not new: 50 | self.load() 51 | self.render() 52 | 53 | def load(self): 54 | with open(self.path, 'rU') as f: 55 | self.content = f.read().decode('utf-8') 56 | 57 | def render(self): 58 | processed = Processors(self.content) 59 | self._html, self.body, self._meta = processed.out() 60 | 61 | def save(self, update=True): 62 | folder = os.path.dirname(self.path) 63 | if folder != "" and not os.path.exists(folder): 64 | os.makedirs(folder) 65 | with open(self.path, 'w') as f: 66 | for key, value in self._meta.items(): 67 | line = u'%s: %s\n' % (key, value) 68 | f.write(line.encode('utf-8')) 69 | f.write('\n'.encode('utf-8')) 70 | f.write(self.body.replace('\r\n', '\n').encode('utf-8')) 71 | if update: 72 | self.load() 73 | self.render() 74 | 75 | @property 76 | def meta(self): 77 | return self._meta 78 | 79 | def __getitem__(self, name): 80 | item = self._meta[name] 81 | if len(item) == 1: 82 | return item[0] 83 | return item 84 | 85 | def __setitem__(self, name, value): 86 | self._meta[name] = value 87 | 88 | @property 89 | def html(self): 90 | return self._html 91 | 92 | def __html__(self): 93 | return self.html 94 | 95 | @property 96 | def title(self): 97 | return self['title'] 98 | 99 | @title.setter 100 | def title(self, value): 101 | self['title'] = value 102 | 103 | @property 104 | def tags(self): 105 | return self['tags'] 106 | 107 | @tags.setter 108 | def tags(self, value): 109 | self['tags'] = value 110 | 111 | 112 | class Wiki(object): 113 | def __init__(self, root): 114 | self.root = root 115 | 116 | def path(self, url): 117 | return os.path.join(self.root, url + '.md') 118 | 119 | def exists(self, url): 120 | path = self.path(url) 121 | return os.path.exists(path) 122 | 123 | def get(self, url): 124 | path = os.path.join(self.root, url + '.md') 125 | if self.exists(url): 126 | return Page(path, url) 127 | return None 128 | 129 | def get_or_404(self, url): 130 | page = self.get(url) 131 | if page: 132 | return page 133 | abort(404) 134 | 135 | def get_bare(self, url): 136 | path = self.path(url) 137 | if self.exists(url): 138 | return False 139 | return Page(path, url, new=True) 140 | 141 | def move(self, url, newurl): 142 | folder = os.path.dirname(newurl) 143 | if folder != "" and not os.path.exists(folder): 144 | os.makedirs(os.path.join(self.root, folder)) 145 | os.rename( 146 | os.path.join(self.root, url) + '.md', 147 | os.path.join(self.root, newurl) + '.md' 148 | ) 149 | 150 | def delete(self, url): 151 | path = self.path(url) 152 | if not self.exists(url): 153 | return False 154 | os.remove(path) 155 | return True 156 | 157 | def index(self, attr=None): 158 | def _walk(directory, path_prefix=()): 159 | if not os.path.isdir(directory): 160 | os.makedirs(directory) 161 | return 162 | for name in os.listdir(directory): 163 | fullname = os.path.join(directory, name) 164 | if os.path.isdir(fullname) and fullname != "upload": 165 | _walk(fullname, path_prefix + (name,)) 166 | elif name.endswith('.md'): 167 | if not path_prefix: 168 | url = name[:-3] 169 | else: 170 | url = os.path.join('/'.join(path_prefix) , name[:-3]) 171 | #url = os.path.join(path_prefix[0], name[:-3]) 172 | if attr: 173 | pages[getattr(page, attr)] = page 174 | else: 175 | if name != "home.md": 176 | pages.append(Page(fullname, url.replace('\\', '/'))) 177 | if attr: 178 | pages = {} 179 | else: 180 | pages = [] 181 | _walk(self.root) 182 | if not attr: 183 | return self._return_indexs_by_sorted(pages) 184 | return pages 185 | 186 | def get_by_title(self, title): 187 | pages = self.index(attr='title') 188 | return pages.get(title) 189 | 190 | def get_tags(self): 191 | pages = self.index() 192 | tags = {} 193 | for page in pages: 194 | pagetags = page.tags.split(',') 195 | for tag in pagetags: 196 | tag = tag.strip() 197 | if tag == '': 198 | continue 199 | elif tags.get(tag): 200 | tags[tag].append(page) 201 | else: 202 | tags[tag] = [page] 203 | return tags 204 | 205 | def index_by_tag(self, tag): 206 | pages = self.index() 207 | tagged = [] 208 | for page in pages: 209 | if tag in page.tags: 210 | tagged.append(page) 211 | return self._return_indexs_by_sorted(tagged) 212 | 213 | def search(self, term, attrs=['title', 'tags', 'body']): 214 | pages = self.index() 215 | regex = re.compile(term) 216 | matched = [] 217 | for page in pages: 218 | for attr in attrs: 219 | if regex.search(getattr(page, attr)): 220 | matched.append(page) 221 | break 222 | return self._return_indexs_by_sorted(matched) 223 | 224 | def _return_indexs_by_sorted(self, indexs): 225 | return sorted(indexs, key=lambda x: x.url.lower(), reverse=True) 226 | 227 | 228 | class UserManager(object): 229 | def __init__(self, path): 230 | if os.path.exists("content/user_users.json"): 231 | self.file = os.path.join(path, 'user_users.json') 232 | else: 233 | self.file = os.path.join(path, 'users.json') 234 | 235 | def read(self): 236 | if not os.path.exists(self.file): 237 | return {} 238 | with open(self.file) as f: 239 | data = json.loads(f.read()) 240 | return data 241 | 242 | def write(self, data): 243 | with open(self.file, 'w') as f: 244 | f.write(json.dumps(data, indent=2)) 245 | 246 | def add_user(self, name, password, 247 | active=True, roles=[], authentication_method=None): 248 | users = self.read() 249 | if users.get(name): 250 | return False 251 | if authentication_method is None: 252 | from app import get_default_authentication_method 253 | authentication_method = get_default_authentication_method() 254 | new_user = { 255 | 'active': active, 256 | 'roles': roles, 257 | 'authentication_method': authentication_method, 258 | 'authenticated': False 259 | } 260 | if authentication_method == 'hash': 261 | new_user['hash'] = make_salted_hash(password) 262 | elif authentication_method == 'cleartext': 263 | new_user['password'] = password 264 | else: 265 | raise NotImplementedError(authentication_method) 266 | users[name] = new_user 267 | self.write(users) 268 | userdata = users.get(name) 269 | return User(self, name, userdata) 270 | 271 | def get_user(self, name): 272 | users = self.read() 273 | userdata = users.get(name) 274 | if not userdata: 275 | return None 276 | return User(self, name, userdata) 277 | 278 | def delete_user(self, name): 279 | users = self.read() 280 | if not users.pop(name, False): 281 | return False 282 | self.write(users) 283 | return True 284 | 285 | def update(self, name, userdata): 286 | data = self.read() 287 | data[name] = userdata 288 | self.write(data) 289 | 290 | 291 | class User(object): 292 | def __init__(self, manager, name, data): 293 | self.manager = manager 294 | self.name = name 295 | self.data = data 296 | 297 | def get(self, option): 298 | return self.data.get(option) 299 | 300 | def set(self, option, value): 301 | self.data[option] = value 302 | self.save() 303 | 304 | def save(self): 305 | self.manager.update(self.name, self.data) 306 | 307 | def is_authenticated(self): 308 | return self.data.get('authenticated') 309 | 310 | def is_active(self): 311 | return self.data.get('active') 312 | 313 | def is_anonymous(self): 314 | return False 315 | 316 | def get_id(self): 317 | return self.name 318 | 319 | def check_password(self, password): 320 | 321 | authentication_method = self.data.get('authentication_method', None) 322 | if authentication_method is None: 323 | from app import get_default_authentication_method 324 | authentication_method = get_default_authentication_method() 325 | if authentication_method == 'hash': 326 | result = check_hashed_password(password, self.get('hash')) 327 | elif authentication_method == 'cleartext': 328 | result = (self.get('password') == password) 329 | else: 330 | raise NotImplementedError(authentication_method) 331 | return result 332 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=0.9 2 | Flask-Login>=0.1.3 3 | Flask-Script>=0.5.1 4 | Flask-WTF>=0.8 5 | Jinja2>=2.6 6 | Markdown>=2.2.0 7 | Pygments>=1.5 8 | WTForms>=1.0.2 9 | Werkzeug>=0.8.3 10 | gunicorn>=0.14.6 11 | wsgiref>=0.1.2 -------------------------------------------------------------------------------- /static/ajaxfileupload.js: -------------------------------------------------------------------------------- 1 | 2 | jQuery.extend({ 3 | 4 | 5 | createUploadIframe: function(id, uri) 6 | { 7 | //create frame 8 | var frameId = 'jUploadFrame' + id; 9 | var iframeHtml = '