910JQK / linuxbar

A lightweight forum app based on Flask.
MIT License
19 stars 4 forks source link

Improvements #3

Open pyx opened 7 years ago

pyx commented 7 years ago

好久不見,決定做輪子啦。 我剛剛看了一下現在的 code base,覺得好多地方可以更好,包括風格,架構,減少重複代碼,更 pythonic 等,有時間我可以一個個 pull request 來。 在此之前,我決定將上次弄的半成品(我發過截圖那個)放出來,或許你可以參考一下。 純 python 和 hy 的 port 都放。 i18n 和 log 可以很簡單寫好 production-ready 的代碼,大概幾十行內就行,但我不確定什麼時候有時間準備 pull request, 另外一些設計決定,如果你有興趣的話我們可以再深入討論一下。

pyx commented 7 years ago

臨時整理了一下,python 的放出來了 https://github.com/pyx/arsenal 這個 demo 的 logging 和 i18n 當時沒打算弄,而完整的不方便公開,我有時間另外發個 pull request 吧

910JQK commented 7 years ago

8pm 大師~ 不得不承認我的 code 確實不大行,有些地方寫的時候也意識到了,但不知道怎麽改。前輩能蒞臨指導,那真是再好不過啦。 至於設計決定,我明天寫個 README 好了(雖然有所規劃,在貼吧我也沒說很多)。

pyx commented 7 years ago

我覺得首先是目標吧,你應該有腹稿的了,確實有必要寫出來,但不一定在 README,比如

總體原則: KISS,即貼吧 circa 2006-2010

計劃要完成的 features:

可能有用的 features:

deployment & hosting 方案:

coding 策略與 coding style:

這些先大概定下來會好很多,而且基本上都影響 lib 的選擇和 coding 方式

README 主要是簡單說 what, how 就行了 另外可考慮選個 license,沒有等於 all right reserved 的。

下面分開說一下細節,想到哪寫到哪

pyx commented 7 years ago

首先說架構吧

考慮儘可能自動化,比如新開發者怎麼配置好環境等

至少要準備 requirements.txt and/or setup.py,requirements 甚至可以細分,比如 end user 的和開發者的,開發者的應該多了 testing 工具的依賴之類

初始數據庫等也應早考慮寫成腳本

我將那個半成品放出來目的就是覺得應該對你有參考價值,用 make, fabric, flask-script 等自動化這些過程,其中 flask-script 目前有可能被 flask 0.11 開始的功能取代,比如 flask 這個 command 和 click 寫 cli。

目的就是新用戶/開發者應該能:

git clone https://github.com/910JQK/linuxbar/ REPOS
cd REPOS
# mkvirtualenv etc.
pip install -r requirements.txt
$EDITOR some-config-py
./manage.py init_db  # etc.
./manage.py run

馬上就能用之類的

pyx commented 7 years ago

然後是一些 code 的問題吧,我一個個文件按順序來看

app.py:


import https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L4-L5

我個人覺得 \ 不好看也容易出錯,可以用括號,這樣怎麼分行都方便,如:

from flask import (Flask, Response, request, session, redirect, url_for,
                   render_template, send_file, abort)

flask app https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L7-L8

建議使用 app factory,方便以後測試和 deploy 見:http://flask.pocoo.org/docs/0.11/patterns/appfactories/


local config https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L26-L39

config 最好單獨一個 module,比如 config.py 或 dev_config.py 或 default_config.py 之類的,依賴關係和 overwrite 可以用 import 解決,另外還有 class-based 的,用 class 當 namespace 使用,不過我個人看法覺得是 abuse 了,雖然寫起來不錯。 可以參考我那個的 config 處理和這裡: http://flask.pocoo.org/docs/0.11/config/#configuration-best-practices


def _ https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L49-L51

每個文件裡的 _,其實可以放在一個工具 module 裡面,叫 utils.py 或者 common.py 或者 tools.py 什麼的 每個文件裡面就可以改成

from .utils import _

等 i18n 設好以後,將這一行的 .utils 改成 i18n 的就行

另外,配置好 i18n 也是很簡單的,提取/更新 .po 文件也可以用 make 或 fabric 或 manage.py 自動起來


def send_mail https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L54-L76

Flask-Mail 可以簡化這個 https://pypi.python.org/pypi/Flask-Mail 最主要還可以支持測試,即 TESTING 時不寄出,並可以存入一個 outbox 變量方便檢測內容


def check_permission https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L79-L102

首先我認為這個應該屬於 auth 或 user 或 utils 甚至 db (其實我認為 models 可能更合適) 的 module 里,app.py 應該只有關於 app 創建等的內容

然後,如果這個函數都在 view function 里用,可以直接 abort(401),401/404 的界面可以做得很 fancy 的。


def json_response https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L105-L114

http://flask.pocoo.org/docs/0.11/api/#flask.json.jsonify 如果要提供 REST API,還有兩三個 extension 可選


error message handling https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L117-L147 還沒看到怎麼用的,直覺感覺沒必要,one-liner


made config public to templates https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L150-L152 還沒看到其他文件,不過一般其他 module, template 知道的越少越好,不會有意外的依賴,so,先看下去


smart date repr. https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L155-L202

第一反映是這個不需要自己寫的,比如 dev 版本的 babel 有:

flask_babel.format_timedelta

然後是這個應該放在 utils.py 之類的 再然後是,我個人不太喜歡後台 render 這個,主要原因有二, 一 用戶 timezone 不好處理,用戶自設嗎,還是 geoip,只為 eye-candy 都太 heavy 了 二 後台 render 的,存下 html 以後會不準確

所以如果我要使用 smart date 的,一般用 front-end 手段,如 Moment.js,有 extension 方便使用: https://pypi.python.org/pypi/Flask-Moment


view index https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L205-L207

either redirect or:

@app.route('/')
@app.route('/board/<name>')
def board(name=''):
    ....

view board https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/app.py#L210-L242

看到這裡,我發現我犯了個錯誤,光這個文件就 1300+ loc,看來我不能這樣搞 :(

轉下一貼

pyx commented 7 years ago

現在開始就粗一點說 鼠標亂飄 :) 看到什麼說什麼


自己寫 pagination,其實 peewee 有 flask extension,http://pypi.python.org/pypi/Flask-Peewee 提供了 pagination 還有其他功能的,但已經是維護模式了。 這就帶來另外一個問題了,選 peewee 的理由,我個人 prefer SQLAlchemy,雖然配置麻煩點,但上限高,免得以後換,Flask-SQLAlchemy http://pypi.python.org/pypi/Flask-SQLAlchemy 也提供整合和 pagination 等,而且是 Armin 自己維護。


應該盡量避免用 magic number,特別是兩層或以上的 collection 嵌套,解決方法我會這用兩種

  1. OOP Style - namedtuple https://docs.python.org/3/library/collections.html#collections.namedtuple

    Result = collections.namedtuple('Result', 'code message')
    
    def f():
      return Result(code=42, message='answer')
    
    res = f()
    assert res.code == 42
    assert res.message == 'answer'
  2. Functional Style - data accessor

    def getter(*path):
      # omit error handling here for brevity's sake
      def get(collection):
          for p in path:
              collection = collection[p]
          return collection
     return get
    
    # = lambda c: c[2]['content']
    get_content = getter(2, 'content')
    # = lambda c: c[42]['foobar']
    get_foobar = getter(42, 'foobar')

多利用 python 的 first-class function 這個特點,比如這裡 https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/forum.py#L34-L35 instead of

def now():
    return datetime.datetime.now()

可以這樣

now = datetime.datetime.now

沒必要多一層

if len(lst) == 0:
    pass
# 直接用
if not lst:
    pass

等等


User handling,用現有的 extension 可以省很多代碼,而且和其他 extension 和 flask 本身整合更好

最基礎的 login/logout 有 Flask-Login http://pypi.python.org/pypi/Flask-Login 用戶權限,有 Flask-Principal https://pypi.python.org/pypi/Flask-Principal 更完整的,包括 activation,重置密碼等,有 Flask-Security https://pypi.python.org/pypi/Flask-Security ,是整合了上面兩個和其他的,大概相當於 django 的 auth app


Sending email 應該異步,不要用 blocking call,否則會是個很容易 DDoS 的點,最簡單的是 multiprocessing 或 threading 的 Pool,或者 third-party 的如 celery


encryption 等 最好不要自己寫,即使 hash + salt,而且為防 timing-attack,應該用專門 hash password 的,那些會迭代很多次,最省事是用 werkzeug (Flask 依賴的 WSGI 實現)的,

from werkzeug.security import check_password_hash, generate_password_hash

Uploading Flask-Uploads http://pypi.python.org/pypi/Flask-Uploads


User input validation, form handling Flask-WTF http://pypi.python.org/pypi/Flask-WTF 支持 CSRF,而且和各個 ORM extension 整合很好


永遠不要信任用戶輸入,存入時應該 sanitize,去掉所有 html tags,特別是 script 和 iframe,escape sequence,處理麻煩的 unicode entity,目前的代碼就沒做好 早期貼吧就因為這些常出漏洞

一個相關的內容是,我強烈建議給用戶輸入 Markdown,並且支持 code highlighting,再 sanitize 掉有害的內容(Markdown 支持直接 html),最後再轉換 @ 的連接,除了最後一步,具體怎麼做可參考我那個 forum。

即使自己寫轉換,也很多地方可以精簡的,比如: https://github.com/910JQK/linuxbar/blob/1b92a2390d08bc82d7621beafa8ea2d279611761/forum.py#L66-L113 這種情況,現在是寫成 imperative 的 FSM 還不如寫成 functional 的 data pipeline 可以隨意搭配 每步處理多的用 generator 也不特別耗費資源 e.g

# in pipeline.py
def normalize_newline(text):
    return '\n'.join(text.splitlines())

BEGIN_CODE = '/***#'
END_CODE = '#***/'

def normalize_space(text):
    in_code_block = 0
    for line in text:
        if line.strip().startswith(BEGIN_CODE):
             in_code_block += 1
        if not in_code_block:
            yield ' '.join(line.split())
        else:
            yield line
        if line.strip().endswith(END_CODE):
            in_code_block -= 1

def other_transformation(text):
    for line in text:
        # blah blah blah
        yield line

def pipeline(*processors):
    def process(text):
        for proc = processors:
            text = proc(text)
        return text
    return process

# in forum.py
from .pipeline import (
    normalize_newline,
    normalize_space,
    other_transformation,
    pipeline,
)

process_content = pipeline(
    normalize_newline,
    normalize_space,
    other_transformation,
)

...
    return process_content(result[0]['content']))

當然,上面說了,這個具體情況下,直接用 Markdown parser 更方便,client side preview 有很多選擇


Rendering 代碼實在太多,我沒看完,但感覺是直接 json,然後 client side rendering,我個人看法是這個應用中不好,原因是

至於 RESTful API,還是可以提供,只要使用 flask extension 的 ORM 和 REST 框架,可以自動生成 API 的,一般並不需要重複寫 views


用 blueprint http://flask.pocoo.org/docs/0.11/blueprints/


模塊化 應盡量分類放,每個 module 盡量保持 200 loc 以下,否則會嚴重影響寫代碼的 productivity 我個人的標準是不超過 100,連注釋,合理分類一般能做到,除了個別模塊,比如想將所有 Model 放在一起。

我寫過一個類似規模的 project,連 100% test coverage 的 tests 代碼,連注釋,連其他 script,總共才 3000 loc python,其中 1000 多是 tests,200 是 script。各個 module 20-80 不等,只有 models 和 views 我為了減少相互 import 故意放在一起,一個 400 多,一個 200 多,但都很好閱讀,因為都是同類的。


最後

目前這個代碼太多了,建議先合理分類好模塊,重構一下,精簡代碼,現在是 3695 行 python,我估計可以精簡至少一半,然後再看看。 我如果還有時間再看具體的地方。

910JQK commented 7 years ago

信息量略大…… 從開始寫到現在一直都是走路不看路,結構確實亂。這幾天先精簡一下代碼,搞搞模塊化。其它的慢慢來,一點一點修。 P.S. 除了樓中樓翻頁以外沒什麽 client side rendering 了,寫一坨 API 是真不知道有庫(準確地說是沒有意識去找,各種強寫

910JQK commented 7 years ago

試着重構了幾天,感覺腦子要炸了,搞不下去了…… https://github.com/910JQK/linuxbar/compare/dev 事情是這樣的:我去看了看生成 API 的庫,發現難以滿足以下需求:

然後我就想把 forum.py 模塊化,然後自動生成 API, 把重複寫的 view 去掉。結果越寫越玄乎,感覺有些東西難以處理。 比如用 None 代表「不限板塊」(全局 admin / ban),但由於 bool(None) == False, 會造成版塊不存在的錯誤,所以就 hack 來 hack 去,整個都亂了。有些不斷重複的代碼也不知道怎麽改。對這些具體問題很頭疼。各個表之間邏輯關係太多,腦子繞不過來,庫也沒法用。 感覺是各種姿勢不對。

於是我想,把原來的代碼先拆開,把要緊的問題小修一下,再把剩下的功能完成(基本就是些前端的事),先能用再說,然後再研究怎麽重構。

pyx commented 7 years ago

我沒注意到原來在 dev branch 有改動

把原來的代碼先拆開,把要緊的問題小修一下,再把剩下的功能完成(基本就是些前端的事),先能用再說,然後再研究怎麽重構。

這個會造成 technical debt,你現在代碼這麼新鮮都看/改不下去,六個月後的自己更無法理解了,也就是說,一個根據經驗很合理的推測是,現在這個 code base,開發者以後基本上很難會花比現在少的力氣改進,或者在這基礎上更加一些新功能,或者定位查找 bugs,換言之,無法維護。

因爲我上次留言還沒看到 README,沒法本地測試(這就是項目架構先搞好的重要性,我想測試一下,發些小 pull request,沒門路才開始寫上面一大段的),不知道完成度怎麼樣,如果你覺得基本功能都完成了,那我覺得最好的下一步是重寫,完全重頭來。在沒有 deadline 的情況下,依照之前寫過的經驗,重寫一個往往比全盤重構更省事,而且最終會得到更好的代碼,因爲之前走過的(時間和設計上的)彎路都能避免了。

一步步重頭來過,看似麻煩,其實更快,如果是我,會這樣做

如果之前沒試過,先通讀一遍 Flask 的文檔,包括後面的 cookbook 和 extension 介紹,需時大概一兩天吧,主要瞭解有什麼提供的和有什麼注意事項,best practice 什麼的,效果達到需要時知道大概哪裏找出來看細節就行。(題外話,閱讀也能提高 coding 水平的,我近十年來的提高,大量閱讀是最重要的因素)

  1. 選個好點的名字,PyPI 上沒人用的,先註冊下來,以後不用改了,linuxbar 有點 low
  2. 項目基本文件和目錄結構先弄好,比如 README, LICENSE, requirements.txt,目的是方便構建開發環境
  3. 寫個基本 app,就一個 index,return 'hello world' 就行
  4. 數據庫設計好,寫 tests,我推薦用 py.test,主要測試基本操作,比如用戶建立啦(可以先不考慮驗證),用戶發帖啦,用戶編輯帖子啦,暫時先不測試權限
  5. 可以考慮先用 Flask-Login 弄好簡單的 session-based 的登錄部分,做成一個 blueprint,叫 auth 之類的,然後註冊到 app,比如掛在 '/auth' 下面,然後將這個依賴添加到 requirements.txt,這個過程也可以用 TDD 的方法,先寫好 tests。
  6. 重複 5,分別實現 board 的顯示,admin,註冊,用戶 profile,每個都是一個 blueprint,逐個添加

雖然目錄架構,各文件等沒有 100% 通用的標準,相信你看過我上面發的那個論壇應該對你怎麼構建項目的結構(文件,模塊等)會有幫助。

由於具體代碼我不方便公開,我貼一些片段,或許可以給你一些靈感

例如測試 user model 的 tests/test_user.py 全文是:

# -*- coding: utf-8 -*-
import pytest
import sqlalchemy.exc

from carucate.models import db, User

def test_create_user(app):
    john = User(email='john@example.com', password='1234')
    db.session.add(john)
    db.session.commit()
    user = User.query.filter_by(email='john@example.com').one()
    assert user.check_password('1234')

def test_unique_user_email(app):
    john = User(email='john@example.com', password='1234')
    db.session.add(john)
    db.session.commit()
    john2 = User(email='john@example.com', password='5678')
    db.session.add(john2)
    with pytest.raises(sqlalchemy.exc.IntegrityError):
        db.session.commit()

def test_user_password_unreadable(app):
    john = User(email='john@example.com', password='1234')
    with pytest.raises(AttributeError):
        'password is: ' + john.password

def test_user_check_password(app):
    john = User(email='john@example.com', password='1234')
    assert john.check_password('1234')
    assert not john.check_password('1234.')

例如 測試 auth 這個 blueprint 的 views 大約是這樣的:

# -*- coding: utf-8 -*-
import re

from flask import url_for

from carucate.mail import mail
from carucate.models import db, User
def parse_url(text):
    return re.findall(r'http://localhost/\S+', text)[0]

def test_change_email(app, user, login):
    with app.test_client() as client, mail.record_messages() as outbox:
        login(client)
        response = client.post(
            url_for('auth.change_email'),
            follow_redirects=True,
            data=dict(email='jane@example.com'))
        html = response.data.decode()
        assert 'confirmation email has been sent to you' in html

        response = client.get(url_for('auth.logout'), follow_redirects=True)
        html = response.data.decode()
        assert 'Logged out.' in html

        assert len(outbox) == 1
        url = parse_url(outbox[0].body)
        response = client.get(url, follow_redirects=True)
        html = response.data.decode()
        assert response.status_code == 200
        assert 'email address confirmed' in html

        response = client.post(
            url_for('auth.login'),
            follow_redirects=True,
            data=dict(email='john@example.com', password='1234'))
        html = response.data.decode()
        assert 'Invalid email or password' in html

        response = client.post(
            url_for('auth.login'),
            follow_redirects=True,
            data=dict(email='jane@example.com', password='1234'))
        html = response.data.decode()
        assert 'Logged in successfully.' in html

def test_change_email_with_get_should_fail(app, user, login):
    with app.test_client() as client:
        login(client)
        response = client.get(
            url_for('auth.change_email'),
            follow_redirects=True,
            query_string=dict(email='jane@example.com'))
        assert response.status_code == 200
        assert User.query.one().email == 'john@example.com'

def test_change_password(app, user, login):
    with app.test_client() as client:
        login(client)
        response = client.post(
            url_for('auth.change_password'),
            follow_redirects=True,
            data=dict(password='incorrect', newpass='4321', confirm='4321'))
        html = response.data.decode()
        assert 'Incorrect password' in html
        assert user.check_password('1234')

        client.post(
            url_for('auth.change_password'),
            follow_redirects=True,
            data=dict(password='1234', newpass='4321', confirm='4321'))
        assert user.check_password('4321')

...

def test_invalid_activation_token(app):
    with app.test_client() as client:
        response = client.get(
            url_for('auth.activate_account', token='invalid'),
            follow_redirects=True)
        assert response.status_code == 404

def test_invalid_login(app):
    with app.test_client() as client:
        response = client.post(
            url_for('auth.login'),
            follow_redirects=True,
            data=dict(email='john@example.com', password='1234'))
        html = response.data.decode()
        assert 'Invalid email or password' in html

def test_login_logout(app):
    john = User(email='john@example.com', password='1234')
    john.is_active = True
    john.email_confirmed = True
    db.session.add(john)
    db.session.commit()
    with app.test_client() as client:
        response = client.post(
            url_for('auth.login'),
            follow_redirects=True,
            data=dict(email='john@example.com', password='1234'))
        html = response.data.decode()
        assert 'Logged in successfully.' in html

        response = client.get(url_for('auth.logout'), follow_redirects=True)
        html = response.data.decode()
        assert 'Logged out.' in html

...

比如 auth blueprint 的 views (auth/views.py) 裏部分內容是這樣的

from flask import (
    Blueprint,
    abort,
    current_app,
    flash,
    redirect,
    render_template,
    request,
    url_for,
)
from flask_login import (
    LoginManager,
    current_user,
    login_required,
    login_user,
    logout_user,
)
from itsdangerous import URLSafeTimedSerializer

from .forms import (
    ChangeEmailForm,
    ChangeNameForm,
    ChangePasswordForm,
    ForgetPasswordForm,
    LoginForm,
    ResetPasswordForm,
    SignupForm,
)
from .. import mail
from ..constants import DEFAULT_TOKEN_MAX_AGE, MAX_USER_NAME_LENGTH
from ..models import User, db

login_manager = LoginManager()
login_manager.login_view = 'auth.login'

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

def init_app(app):
    login_manager.init_app(app)

auth = Blueprint('auth', __name__)

...

@auth.route('/activate/<token>/')
def activate_account(token):
    salt = current_app.config['CARUCATE_ACTIVATION_SALT']
    max_age = current_app.config['CARUCATE_ACTIVATION_TOKEN_MAX_AGE']
    email = validate_token_or_404(token, salt, max_age)
    user = User.query.filter_by(email=email).first_or_404()
    user.email_confirmed = True
    user.is_active = True
    db.session.add(user)
    db.session.commit()
    flash('account activated, please login with your email')
    return redirect(url_for('.login'))

...

@auth.route('/login/', methods=['GET', 'POST'])
def login():
    if not current_user.is_anonymous:
        return redirect(url_for('home.index'))

    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if check_password(user, form.password.data):
            login_user(user, form.remember_me.data)
            flash('Logged in successfully.')
            next_ = request.args.get('next')
            return redirect(next_ or url_for('dashboard.index'))
        flash('Invalid email or password')
    return render_template('auth/login.html', form=form)

@auth.route('/logout/')
@login_required
def logout():
    logout_user()
    flash('Logged out.')
    return redirect(url_for('.login'))

...

而 app.py 全文裏是大概這樣的

from flask import Flask
from flask_gravatar import Gravatar
from flask_moment import Moment
from flask_pure import Pure
from flask_simplemde import SimpleMDE
from .config import DefaultConfig
from .auth.views import auth, init_app as auth_init_app
...
from .dashboard.views import dashboard
...
from .group.views import group
from .home.views import home
...
from .user.views import user
from .mail import init_app as mail_init_app
from .models import init_app as models_init_app

def create_app(config=None, config_filename=None):
    """application factory"""
    app = Flask(__name__, instance_relative_config=True)
    if config is None:
        config = DefaultConfig
    app.config.from_object(config)
    if config_filename is not None:
        app.config.from_pyfile(config_filename)
    app.config.from_envvar('CARUCATE_SETTINGS', silent=True)
    mail_init_app(app)
    models_init_app(app)
    auth_init_app(app)
    Gravatar(app, default='identicon')
    Moment(app)
    Pure(app)
    SimpleMDE(app)
    app.register_blueprint(auth, url_prefix='/auth')
    ...
    app.register_blueprint(dashboard, url_prefix='/dashboard')
    ...
    app.register_blueprint(group, url_prefix='/groups')
    app.register_blueprint(home)
    ...
    app.register_blueprint(user, url_prefix='/user')
    return app

部分代碼大小,這是功能完整的了,就是我前面提到的原則,實際上每個 module 超過 200 loc 就變得難以查找和理解(除非是 utils 這類每個 function 之前沒有很緊密結合的工具 module) Python 足夠高級以至於大多數情況下,設計良好的 module 都可以在這個範圍

wc -l `hg manifest | grep '\.py'`
    0 carucate/__init__.py
   50 carucate/app.py
    0 carucate/auth/__init__.py
   67 carucate/auth/forms.py
  235 carucate/auth/views.py
   ...
   36 carucate/config.py
   53 carucate/constants.py
    0 carucate/dashboard/__init__.py
   13 carucate/dashboard/views.py
   52 carucate/decorators.py
   ...
    9 carucate/forms.py
    0 carucate/group/__init__.py
   52 carucate/group/forms.py
   65 carucate/group/views.py
    0 carucate/home/__init__.py
   28 carucate/home/views.py
   ...
   43 carucate/mail.py
   11 carucate/mixins.py
  414 carucate/models.py
   ...
    0 carucate/user/__init__.py
   25 carucate/user/views.py
   34 carucate/utils.py
   35 fabfile.py
   84 manage.py
   30 scripts/create_dev_config.py
   30 scripts/create_production_config.py
   26 setup.py
   ...
pyx commented 7 years ago

我覺得帖子的回覆,要麼就傳統論壇的引用,要麼就支持多層嵌套,像貼吧那種只有一層樓中樓的,我認爲是高不成低不就的做法。

多層嵌套就是每個帖子的 model 做成樹的形式,爲了避免多次遞歸造成的訪問數據庫次數過多,有好些方法避免,上面貼的那個片段的項目我用的是其中一種叫 materialized path,簡單的說就是不用自引用的 foreign key 而是用一個專門的 field 來記錄 path,比如 path='123/456/789',其中數字是 id。materialized path 非常適合用在支持多層嵌套的帖子,排序,插入,刪除,查找子樹,都很低 overhead。

1dot75cm commented 7 years ago

那么窝们就先来规划一下, 然后 PR 什么的慢慢来咯? 窝建议就直接用 Flask web dev 那本书的作者的示例代码结构, 风格非常好哇。 窝自己照着学习一遍都很有收获 https://github.com/1dot75cm/flasky

Mark @pyx 大神的建议好实用

910JQK commented 7 years ago

待我慢慢研究研究……

pyx commented 7 years ago

強烈建議 @1dot75cm 的建議,通讀一遍 Flask Web Development,再開始寫,如果找不到本書,作者有一系列的 blog post,書的內容就是從那裡提煉出來的。

裡面有不少好的建議,包括怎樣組織項目結構,選用 extensions,best practices,雖然有些建議我不見得認同[1],但 Flask 和其他 full stack (Django,RoR,etc.) 比起來,就是 all about choices,值得一看。

[1]:

1dot75cm commented 7 years ago

窝刚把 Flask 资源汇总了一下,供参考。 @910JQK https://github.com/1dot75cm/awesome-flask-cn/blob/master/README-cn.md