├── Procfile ├── static ├── demo.png ├── favicon.ico ├── css │ └── style.css └── js │ └── core.js ├── requirements-dev.txt ├── requirements.txt ├── .gitattributes ├── .gitignore ├── README.md ├── app.py └── templates └── index.html /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn -k gevent app:app 2 | -------------------------------------------------------------------------------- /static/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloflask/todo/master/static/demo.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helloflask/todo/master/static/favicon.ico -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | Flask==0.12.2 2 | Jinja2==2.8 3 | Werkzeug==0.11.11 4 | Flask-SQLAlchemy==2.1 5 | SQLAlchemy==1.1.4 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.12.2 2 | Jinja2==2.8 3 | Werkzeug==0.11.11 4 | Flask-SQLAlchemy==2.1 5 | SQLAlchemy==1.1.4 6 | # packages below were used for Heroku deploy 7 | gunicorn==18.0 8 | gevent 9 | psycopg2 10 | 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Microsoft YaHei", "Open Sans", "Helvetica", Arial, sans-serif; 3 | } 4 | 5 | footer a:hover { 6 | border-bottom: 1px solid black; 7 | } 8 | 9 | .edit-form { 10 | display: none; 11 | } 12 | 13 | .collection-item { 14 | border-bottom: 1px solid #d5d5d5 !important; 15 | } 16 | 17 | .category-name { 18 | font-size: 20px; 19 | font-weight: bold; 20 | } 21 | 22 | .delete-category { 23 | display: none; 24 | } 25 | 26 | .move { 27 | cursor: move; 28 | } 29 | 30 | @media (max-width: 600px) { 31 | .nav-button { 32 | display: none; 33 | } 34 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task5 2 | A to-do list App made with Flask and JavaScript. 3 | 4 | ## DEMO 5 | http://task5.herokuapp.com/ 6 | 7 | ![demo](https://raw.githubusercontent.com/helloflask/todo/master/static/demo.png) 8 | 9 | ## Dependency 10 | Flask-SQLAlchemy==2.1 11 | SQLAlchemy==1.1.4 12 | 13 | ## Installation 14 | First, clone it from Github: 15 | ``` 16 | $ git clone https://github.com/helloflask/todo.git 17 | $ cd todo 18 | ``` 19 | Then use `pip` to install requirements (recommend to use `virtualenv` create a virtual enviroment): 20 | ``` 21 | $ pip install -r requirements-dev.txt 22 | ``` 23 | Run the app: 24 | ``` 25 | $ python app.py 26 | ``` 27 | 28 | Now Go to http://127.0.0.1:5000/ 29 | 30 | ## To do 31 | - Drag to sort items 32 | - Edit category 33 | - Set task's priority 34 | - Set task's date 35 | 36 | ## More details 37 | 关于这个项目的详细介绍:[Flask实践:待办清单](https://zhuanlan.zhihu.com/p/23834410) 38 | 更多关于Flask的原创优质内容,欢迎关注[Hello, Flask!——知乎专栏](https://zhuanlan.zhihu.com/flask) 39 | 40 | ## License 41 | This demo application is licensed under the MIT license: http://opensource.org/licenses/mit-license.php 42 | -------------------------------------------------------------------------------- /static/js/core.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // edit item 3 | $(".edit-btn").on('click', function () { 4 | var itemId = this.id; 5 | $("#item" + itemId).hide(); 6 | $("#form" + itemId).show(); 7 | $(".cancel-btn").click(function() { 8 | $("#form" + itemId).hide(); 9 | $("#item" + itemId).show(); 10 | }); 11 | }); 12 | 13 | //$('.items').sortable({ handle: '.move' }); 14 | $('select').material_select(); 15 | $(".button-collapse").sideNav(); 16 | 17 | // add new item 18 | $("#new-item").click(function() { 19 | if ($("#category-select").val() == null) { 20 | Materialize.toast('分类还没选呢~', 4000) 21 | } else if ($("#item-input").val() == '') { 22 | Materialize.toast('你的todo是空的!', 4000) 23 | } else if (parseInt($("#items-count").html(), 10) >= 15) { 24 | Materialize.toast('Sorry,条目太多了,删掉几个吧。', 3000); 25 | } else { 26 | Materialize.toast('todo添加成功!', 3000, 'rounded'); 27 | document.getElementById('add-item-form').submit() 28 | } 29 | }); 30 | 31 | $("#new-category").click(function() { 32 | if ($("#category-input").val() == '') { 33 | Materialize.toast('你什么都没有填呢~', 4000) 34 | } else if (parseInt($("#category-count").html(), 10) >= 12) { 35 | Materialize.toast('Sorry,分类太多了,删掉几个吧。', 3000); 36 | } else { 37 | Materialize.toast('分类添加成功!', 3000, 'rounded'); 38 | document.getElementById('add-category-form').submit() 39 | } 40 | }); 41 | 42 | 43 | 44 | $(".item-done").click(function() { 45 | $(this).parent().slideUp(); 46 | Materialize.toast('Well Done +1', 3000, 'rounded') 47 | }); 48 | 49 | $(".categories").hover(function() { 50 | $(this).find(".delete-category").show(); 51 | }, function() {$(this).find(".delete-category").hide() 52 | }); 53 | 54 | $(".confirm-btn").click(function() { 55 | Materialize.toast('修改成功~', 3000, 'rounded') 56 | }); 57 | 58 | $(".delete-item").click(function() { 59 | $(this).parent().slideUp(); 60 | Materialize.toast('删除成功~', 3000, 'rounded') 61 | }); 62 | 63 | $(".delete-category").click(function() { 64 | $(this).parent().slideUp(); 65 | Materialize.toast('删除成功~', 3000, 'rounded') 66 | }); 67 | 68 | $(".signin").click(function() { 69 | Materialize.toast('暂未添加用户系统,抱歉~', 3000) 70 | }); 71 | }) 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | try: 4 | import psycopg2 # for heroku deploy 5 | except: 6 | pass 7 | 8 | from flask import Flask, render_template, redirect, url_for, request 9 | from flask_sqlalchemy import SQLAlchemy 10 | 11 | app = Flask(__name__) 12 | app.config['SECRET_KEY'] = 'a secret string' 13 | app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite://') 14 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 15 | # app.config['DEBUG'] = True 16 | db = SQLAlchemy(app) 17 | 18 | 19 | class Item(db.Model): 20 | id = db.Column(db.Integer, primary_key=True) 21 | body = db.Column(db.Text) 22 | category_id = db.Column(db.Integer, db.ForeignKey('category.id'), default=1) 23 | 24 | 25 | class Category(db.Model): 26 | id = db.Column(db.Integer, primary_key=True) 27 | name = db.Column(db.String(64)) 28 | items = db.relationship('Item', backref='category') 29 | 30 | 31 | # only for local test 32 | # @app.before_first_request 33 | def init_db(): 34 | """Insert default categories and demo items. 35 | """ 36 | db.create_all() 37 | inbox = Category(name=u'收件箱') 38 | done = Category(name=u'已完成') 39 | shopping_list = Category(name=u'购物清单') 40 | work = Category(name=u'工作') 41 | item = Item(body=u'看一小时《战争与和平》') 42 | item2 = Item(body=u'晒太阳') 43 | item3 = Item(body=u'写作练习30分钟') 44 | item4 = Item(body=u'3瓶牛奶', category=shopping_list) 45 | item5 = Item(body=u'5个苹果', category=shopping_list) 46 | item6 = Item(body=u'12支铅笔', category=shopping_list) 47 | item7 = Item(body=u'浇花', category=done) 48 | item8 = Item(body=u'完成demo', category=work) 49 | db.session.add_all([inbox, done, item, item2, item3, item4, item5, item6, item7, item8]) 50 | db.session.commit() 51 | 52 | 53 | @app.route('/', methods=['GET', 'POST']) 54 | def index(): 55 | if request.method == 'POST': 56 | body = request.form.get('item') 57 | category_id = request.form.get('category') 58 | category = Category.query.get_or_404(category_id) 59 | item = Item(body=body, category=category) 60 | db.session.add(item) 61 | db.session.commit() 62 | return redirect(url_for('category', id=category_id)) 63 | return redirect(url_for('category', id=1)) 64 | 65 | 66 | @app.route('/category/') 67 | def category(id): 68 | category = Category.query.get_or_404(id) 69 | categories = Category.query.all() 70 | items = category.items 71 | return render_template('index.html', items=items, 72 | categories=categories, category_now=category) 73 | 74 | 75 | @app.route('/new-category', methods=['GET', 'POST']) 76 | def new_category(): 77 | name = request.form.get('name') 78 | category = Category(name=name) 79 | db.session.add(category) 80 | db.session.commit() 81 | return redirect(url_for('category', id=category.id)) 82 | 83 | 84 | @app.route('/edit-item/', methods=['GET', 'POST']) 85 | def edit_item(id): 86 | item = Item.query.get_or_404(id) 87 | category = item.category 88 | item.body = request.form.get('body') 89 | db.session.add(item) 90 | db.session.commit() 91 | return redirect(url_for('category', id=category.id)) 92 | 93 | 94 | @app.route('/edit-category/', methods=['GET', 'POST']) 95 | def edit_category(id): 96 | category = Category.query.get_or_404(id) 97 | category.name = request.form.get('name') 98 | db.session.add(category) 99 | db.session.commit() 100 | return redirect(url_for('category', id=category.id)) 101 | 102 | 103 | @app.route('/done/', methods=['GET', 'POST']) 104 | def done(id): 105 | item = Item.query.get_or_404(id) 106 | category = item.category 107 | done_category = Category.query.get_or_404(2) 108 | done_item = Item(body=item.body, category=done_category) 109 | db.session.add(done_item) 110 | db.session.delete(item) 111 | db.session.commit() 112 | return redirect(url_for('category', id=category.id)) 113 | 114 | 115 | @app.route('/delete-item/') 116 | def del_item(id): 117 | item = Item.query.get_or_404(id) 118 | category = item.category 119 | if item is None: 120 | return redirect(url_for('category', id=1)) 121 | db.session.delete(item) 122 | db.session.commit() 123 | return redirect(url_for('category', id=category.id)) 124 | 125 | 126 | @app.route('/delete-category/') 127 | def del_category(id): 128 | category = Category.query.get_or_404(id) 129 | if category is None or id in [1, 2]: 130 | return redirect(url_for('category', id=1)) 131 | db.session.delete(category) 132 | db.session.commit() 133 | return redirect(url_for('category', id=1)) 134 | 135 | 136 | if __name__ == '__main__': 137 | app.run() 138 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Task5 7 | 8 | 9 | 10 | 11 | 12 | 13 | 35 | 36 | {{ categories|length }} 37 | {{ items|length }} 38 | 39 |

40 |
41 |
42 |
43 |
44 | 56 |
57 |
58 | 60 | addOK 61 |

62 |
63 |
64 |
65 | 66 |
67 |
68 | 74 |
75 |
76 | {% for category in categories[2:] %} 77 | 78 | x 79 | {{ category.items|length }}{{ category.name }} 80 | 81 | {% endfor %} 82 |
83 |
84 |
85 |
86 |
87 | 88 | 添加 89 |
90 |
91 |
92 |
93 | 94 |
95 | {{ category_now.name }} 96 | {% for item in items %} 97 |
98 |

99 | {% if item.category.id == 2 %} 100 | done_all 101 | {{ item.body }} 102 | {% else %} 103 | done 104 | {{ item.body }} 105 | {% endif %} 106 | delete 107 | mode_edit 108 |

109 |
110 | 111 | 确定 112 | 取消 113 |
114 |
115 | {% endfor %} 116 |
117 |
118 |
119 | 120 | 121 | 122 | 123 | 146 | --------------------------------------------------------------------------------