From 1bbae24f44aa8e2575bdc86e745680acbcddcbf8 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sat, 17 Nov 2012 01:01:28 +0800 Subject: [PATCH 001/119] =?UTF-8?q?=E5=9C=A8=E6=B7=BB=E5=8A=A0=E6=B4=BB?= =?UTF-8?q?=E5=8A=A8=E9=A1=B5=E9=9D=A2=E6=B7=BB=E5=8A=A0=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 将地址和地标选择的功能集成到活动页面中 2. 阻止地址框中输入回车提交表单 3. 更新 _helpers.html 更新 style_tag 和 script_tag 可以接受外部地址 --- website/scriptfan/static/css/activity.css | 8 +++ website/scriptfan/static/js/activity.js | 72 +++++++++++++++++++ website/scriptfan/templates/_helpers.html | 10 +-- .../scriptfan/templates/activities/_form.html | 1 + .../templates/activities/create.html | 9 +++ 5 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 website/scriptfan/static/css/activity.css create mode 100644 website/scriptfan/static/js/activity.js diff --git a/website/scriptfan/static/css/activity.css b/website/scriptfan/static/css/activity.css new file mode 100644 index 0000000..5f14e18 --- /dev/null +++ b/website/scriptfan/static/css/activity.css @@ -0,0 +1,8 @@ +#map-canvas { + width: 600px; + height: 400px; +} + +#map-canvas img { + max-width: none; +} diff --git a/website/scriptfan/static/js/activity.js b/website/scriptfan/static/js/activity.js new file mode 100644 index 0000000..114add3 --- /dev/null +++ b/website/scriptfan/static/js/activity.js @@ -0,0 +1,72 @@ +var xian = new google.maps.LatLng(34.198564,108.895614); +var map, marker, input_address, autocomplete, search; + +function initialize() { + // Initialize map instance + var map_canvas = document.getElementById('map-canvas'); + var map_options = { + zoom: 15, + mapTypeId: google.maps.MapTypeId.ROADMAP, + center: xian + }; + map = new google.maps.Map(map_canvas, map_options); + + // Initialize autocomplete instance + var ac_options = { + bounds: map.getBounds() + // types: ['establishment'] + }; + input_address = document.getElementById('address'); + autocomplete = new google.maps.places.Autocomplete(input_address, ac_options); + autocomplete.bindTo('bounds', map); + autocomplete.addListener('place_changed', place_changed); + + // Initialize marker instance + marker = new google.maps.Marker({ + // map:map, + draggable: true, + animation: google.maps.Animation.DROP + // position: xian + }); + marker.addListener('dragend', marker_dragend); + google.maps.event.addListener(marker, 'click', toggle_bounce); +} + +function place_changed() { + var position = autocomplete.getPlace().geometry.location; + show_position(position); + marker.setPosition(position); + marker.setMap(map); + // map.setCenter(position); + map.panTo(position); + return false; +} + +function marker_dragend() { + var position = marker.getPosition(); + show_position(position); + // map.setCenter(position); + map.panTo(position); // smoothly +} + + +function show_position(position) { + $('#latitude').val(position.lat()); + $('#longitude').val(position.lng()); +} + +function toggle_bounce() { + if (marker.getAnimation() != null) { + marker.setAnimation(null); + } else { + marker.setAnimation(google.maps.Animation.BOUNCE); + } +} + +$(function() { + initialize(); + // 阻止地址输入框回车时提交表单 + $('#address').keydown(function(event) { + return event.keyCode != 13; + }); +}); diff --git a/website/scriptfan/templates/_helpers.html b/website/scriptfan/templates/_helpers.html index 6b083f0..3cb7c4c 100644 --- a/website/scriptfan/templates/_helpers.html +++ b/website/scriptfan/templates/_helpers.html @@ -1,7 +1,7 @@ -{% macro script_tag(filename, scope=None) -%} - +{% macro script_tag(filename, scope=None, external=false) -%} + {%- endmacro %} -{%- macro style_tag(filename, scope=None) -%} - -{%- endmacro %} \ No newline at end of file +{%- macro style_tag(filename, scope=None, external=false) -%} + +{%- endmacro %} diff --git a/website/scriptfan/templates/activities/_form.html b/website/scriptfan/templates/activities/_form.html index 8ce3ceb..e668605 100644 --- a/website/scriptfan/templates/activities/_form.html +++ b/website/scriptfan/templates/activities/_form.html @@ -25,6 +25,7 @@
{{ form.address(class='input-xxlarge') }} +
diff --git a/website/scriptfan/templates/activities/create.html b/website/scriptfan/templates/activities/create.html index 639723f..f7f53bd 100644 --- a/website/scriptfan/templates/activities/create.html +++ b/website/scriptfan/templates/activities/create.html @@ -2,6 +2,15 @@ {% block title %}创建活动{% endblock %} +{% block styles %} +{{ tags.style_tag('css/activity.css') }} +{% endblock %} + +{% block scripts %} +{{ tags.script_tag('http://maps.google.com/maps/api/js?v=3.exp&sensor=false&libraries=places', external=true) }} +{{ tags.script_tag('js/activity.js') }} +{% endblock %} + {% block content %}
{% endblock %} diff --git a/website/scriptfan/templates/base.html b/website/scriptfan/templates/base.html index 791bc74..5fb3136 100644 --- a/website/scriptfan/templates/base.html +++ b/website/scriptfan/templates/base.html @@ -57,7 +57,7 @@ {% for category, message in get_flashed_messages(with_categories=true) %}
- {{ message }} + {{ message | safe }}
{% endfor %}
diff --git a/website/scriptfan/utils/filters.py b/website/scriptfan/utils/filters.py index ba36d56..8616fa1 100644 --- a/website/scriptfan/utils/filters.py +++ b/website/scriptfan/utils/filters.py @@ -1,7 +1,7 @@ #!/usr/bin/env python #-*-coding:utf-8-*- from datetime import datetime - +import markdown2 def dateformat(value, format="%Y-%m-%d %H:%M"): return value.strftime(format) @@ -13,6 +13,8 @@ def empty(value, text=None): return text return value +def markdown(value): + return value and markdown2.markdown(value) or '' def error_class(filed): """ 用于显示 bootstrap 表单的 control-group 中添加 ``error`` 类 diff --git a/website/scriptfan/views/activity.py b/website/scriptfan/views/activity.py index c67de04..54450af 100644 --- a/website/scriptfan/views/activity.py +++ b/website/scriptfan/views/activity.py @@ -9,12 +9,14 @@ from scriptfan.models import Activity from flask.ext.login import current_user +from datetime import datetime activityapp = Blueprint("activity", __name__) @activityapp.route('/', methods=['GET']) def index(): - return render_template('activities/index.html') + activities = Activity.query.all() + return render_template('activities/index.html', activities=activities) @activityapp.route('/create', methods=['GET', 'POST']) def create(): @@ -25,6 +27,8 @@ def create(): # 装填用户和创建时间等信息 activity.user_id = current_user.user.id + # TODO: 使用一些让SQLAlchemy能够自动更新模型中的 created_time 和 modified_time + activity.created_time = datetime.now() db.session.add(activity) flash(u'活动%s发布成功.' % form.data.get('title'), 'success') return redirect(url_for('.index')) From d9b21ad1979bf44690fe0bd921ca4bdd76d2e5e4 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sat, 24 Nov 2012 00:49:41 +0800 Subject: [PATCH 005/119] =?UTF-8?q?=E9=87=8D=E5=BB=BA=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=92=8C=E6=B4=BB=E5=8A=A8=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../19f590834366_add_activity_tables.py | 82 ------------------- .../versions/2f640415ab56_create_users.py | 42 ++++++++++ .../versions/30cb49648d54_create_events.py | 47 +++++++++++ .../3473402c38bc_create_user_tables.py | 26 ------ .../43cda5e14cf0_update_user_tables.py | 40 --------- .../560d0f86e21e_update_activity_remo.py | 22 ----- website/scriptfan/models/__init__.py | 8 +- 7 files changed, 92 insertions(+), 175 deletions(-) delete mode 100644 website/migrate/versions/19f590834366_add_activity_tables.py create mode 100644 website/migrate/versions/2f640415ab56_create_users.py create mode 100644 website/migrate/versions/30cb49648d54_create_events.py delete mode 100644 website/migrate/versions/3473402c38bc_create_user_tables.py delete mode 100644 website/migrate/versions/43cda5e14cf0_update_user_tables.py delete mode 100644 website/migrate/versions/560d0f86e21e_update_activity_remo.py diff --git a/website/migrate/versions/19f590834366_add_activity_tables.py b/website/migrate/versions/19f590834366_add_activity_tables.py deleted file mode 100644 index 013bbc3..0000000 --- a/website/migrate/versions/19f590834366_add_activity_tables.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Add activity tables - -Revision ID: 19f590834366 -Revises: 43cda5e14cf0 -Create Date: 2012-11-14 23:31:56.202053 - -""" - -# revision identifiers, used by Alembic. -revision = '19f590834366' -down_revision = '43cda5e14cf0' - -from alembic import op -import sqlalchemy as db - - -def upgrade(): - op.create_table('activities', - db.Column('id', db.Integer, primary_key=True), - db.Column('user_id', db.Integer, db.ForeignKey('users.id')), - db.Column('title', db.String(255)), - db.Column('content', db.Text), - db.Column('slug', db.String(255)), - db.Column('start_time', db.DateTime), - db.Column('end_time', db.DateTime), - db.Column('address', db.String(255)), - db.Column('longitude', db.Numeric(10, 7)), - db.Column('latitude', db.Numeric(10, 7)), - db.Column('created_time', db.DateTime), - db.Column('modified_time', db.DateTime)) - - op.create_table('activity_users', - db.Column('activity_id', db.Integer, db.ForeignKey('activities.id'), primary_key=True), - db.Column('user_id', db.Integer, db.ForeignKey('users.id'), primary_key=True)) - - op.create_table('resources', - db.Column('id', db.Integer, primary_key=True), - db.Column('cser_id', db.Integer, db.ForeignKey('users.id')), - db.Column('filetype', db.String(50)), - db.Column('url', db.String(255)), - db.Column('created_time', db.DateTime), - db.Column('modified_time', db.DateTime)) - - op.create_table('activity_resources', - db.Column('activity_id', db.Integer, db.ForeignKey('activities.id'), primary_key=True), - db.Column('resource_id', db.Integer, db.ForeignKey('resources.id'), primary_key=True)) - - op.create_table('activity_comments', - db.Column('id', db.Integer, primary_key=True), - db.Column('author_name', db.String(50)), - db.Column('author_email', db.String(255)), - db.Column('author_site', db.String(255)), - db.Column('content', db.Text, nullable=False), - db.Column('created_time', db.DateTime), - db.Column('modified_time', db.DateTime), - db.Column('parent_id', db.Integer, db.ForeignKey('activity_comments.id')), - db.Column('user_id', db.Integer, db.ForeignKey('users.id'))) - - op.create_table('topics', - db.Column('id', db.Integer, primary_key=True), - db.Column('name', db.String(255)), - db.Column('inro', db.Text), - db.Column('rate_count', db.Integer, default=0), - db.Column('user_id', db.Integer, db.ForeignKey('users.id'), nullable=False)) - - op.create_table('topic_resources', - db.Column('topic_id', db.Integer, db.ForeignKey('topics.id'), primary_key=True), - db.Column('resource_id', db.Integer, db.ForeignKey('resources.id'), primary_key=True)) - - op.create_table('topic_users', - db.Column('topic_id', db.Integer, db.ForeignKey('topics.id'), primary_key=True), - db.Column('user_id', db.Integer, db.ForeignKey('users.id'), primary_key=True)) - -def downgrade(): - op.drop_table('topic_users') - op.drop_table('topic_resources') - op.drop_table('topics') - op.drop_table('activity_comments') - op.drop_table('activity_resources') - op.drop_table('resources') - op.drop_table('activity_users') - op.drop_table('activities') diff --git a/website/migrate/versions/2f640415ab56_create_users.py b/website/migrate/versions/2f640415ab56_create_users.py new file mode 100644 index 0000000..40c3736 --- /dev/null +++ b/website/migrate/versions/2f640415ab56_create_users.py @@ -0,0 +1,42 @@ +"""create_users + +Revision ID: 2f640415ab56 +Revises: None +Create Date: 2012-11-23 23:37:50.277992 + +""" + +# revision identifiers, used by Alembic. +revision = '2f640415ab56' +down_revision = None + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.create_table('users', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('slug', sa.String(255)), + sa.Column('nickname', sa.String(255)), + sa.Column('password', sa.String(255)), + sa.Column('email', sa.String(255)), + sa.Column('email_security', sa.Integer), + sa.Column('phone', sa.String(255)), + sa.Column('phone_security', sa.Integer), + sa.Column('photo', sa.String(255)), + sa.Column('photo_security', sa.Integer), + sa.Column('motoo', sa.String(255)), + sa.Column('intro', sa.Text), + sa.Column('login_time', sa.DateTime), + sa.Column('created_time', sa.DateTime), + sa.Column('updated_time', sa.DateTime)) + + op.create_table('user_openids', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('provider', sa.String(255)), + sa.Column('user_id', sa.Integer), + sa.Column('openid', sa.String(255))) + +def downgrade(): + op.drop_table('user_openids') + op.drop_table('users') diff --git a/website/migrate/versions/30cb49648d54_create_events.py b/website/migrate/versions/30cb49648d54_create_events.py new file mode 100644 index 0000000..a08316a --- /dev/null +++ b/website/migrate/versions/30cb49648d54_create_events.py @@ -0,0 +1,47 @@ +"""create_events + +Revision ID: 30cb49648d54 +Revises: 2f640415ab56 +Create Date: 2012-11-24 00:11:32.773938 + +""" + +# revision identifiers, used by Alembic. +revision = '30cb49648d54' +down_revision = '2f640415ab56' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('events', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('title', sa.String(255)), + sa.Column('content', sa.Text), + sa.Column('content_html', sa.Text), + sa.Column('address', sa.String(255)), + sa.Column('lat', sa.Numeric(10,7)), + sa.Column('lng', sa.Numeric(10,7)), + sa.Column('status', sa.Integer), + sa.Column('creator_id', sa.DateTime), + sa.Column('created_time', sa.DateTime), + sa.Column('updated_time', sa.DateTime)) + + op.create_table('event_durations', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('event_id', sa.Integer), + sa.Column('date', sa.Date), + sa.Column('start_time', sa.Time), + sa.Column('end_time', sa.Time)) + + op.create_table('event_members', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('event_id', sa.Integer), + sa.Column('member_id', sa.Integer), + sa.Column('status', sa.Integer)) + +def downgrade(): + op.drop_table('event_members') + op.drop_table('event_durations') + op.drop_table('events') diff --git a/website/migrate/versions/3473402c38bc_create_user_tables.py b/website/migrate/versions/3473402c38bc_create_user_tables.py deleted file mode 100644 index 176278b..0000000 --- a/website/migrate/versions/3473402c38bc_create_user_tables.py +++ /dev/null @@ -1,26 +0,0 @@ -"""create user tables - -Revision ID: 3473402c38bc -Revises: None -Create Date: 2012-11-14 22:35:40.666134 - -""" - -# revision identifiers, used by Alembic. -revision = '3473402c38bc' -down_revision = None - -from alembic import op -import sqlalchemy as sa - -def upgrade(): - op.create_table('user_info', - sa.Column('id', sa.Integer, primary_key=True), - sa.Column('motoo', sa.String(255)), - sa.Column('introduction', sa.Text), - sa.Column('phone', sa.String(15), unique=True, nullable=True), - sa.Column('phone_status', sa.Integer, nullable=True), - sa.Column('photo', sa.String(255), nullable=True)) - -def downgrade(): - op.drop_table('user_info') diff --git a/website/migrate/versions/43cda5e14cf0_update_user_tables.py b/website/migrate/versions/43cda5e14cf0_update_user_tables.py deleted file mode 100644 index 4b406ad..0000000 --- a/website/migrate/versions/43cda5e14cf0_update_user_tables.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Update user tables - -Revision ID: 43cda5e14cf0 -Revises: 3473402c38bc -Create Date: 2012-11-14 23:11:34.817678 - -""" - - -revision = '43cda5e14cf0' -down_revision = '3473402c38bc' - -from alembic import op -import sqlalchemy as db -from datetime import datetime - -def upgrade(): - op.create_table('users', - db.Column('id', db.Integer, primary_key=True), - db.Column('email', db.String(50), unique=True, nullable=False), - db.Column('email_status', db.Integer, nullable=True, default=0), - db.Column('nickname', db.String(50), unique=True, nullable=False), - db.Column('password', db.String(50), nullable=True), - db.Column('is_email_verified', db.Boolean, nullable=False, default=True), - db.Column('slug', db.String(50), nullable=True), - db.Column('created_time', db.DateTime, nullable=False, default=datetime.now), - db.Column('modified_time', db.DateTime, nullable=False, default=datetime.now), - db.Column('last_login_time', db.DateTime), - db.Column('privilege', db.Integer, default=3), - db.Column('user_info_id', db.Integer, db.ForeignKey('user_info.id'), nullable=False)) - - op.create_table('user_openids', - db.Column('id', db.Integer, primary_key=True), - db.Column('user_id', db.Integer, db.ForeignKey('users.id'), nullable=False), - db.Column('openid', db.String(255), nullable=False, unique=True), - db.Column('provider', db.String(50), nullable=False)) - -def downgrade(): - op.drop_table('user_openids') - op.drop_table('users') diff --git a/website/migrate/versions/560d0f86e21e_update_activity_remo.py b/website/migrate/versions/560d0f86e21e_update_activity_remo.py deleted file mode 100644 index bf18bc5..0000000 --- a/website/migrate/versions/560d0f86e21e_update_activity_remo.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Update Activity Remove Slug - -Revision ID: 560d0f86e21e -Revises: 19f590834366 -Create Date: 2012-11-18 23:45:36.244694 - -""" - -# revision identifiers, used by Alembic. -revision = '560d0f86e21e' -down_revision = '19f590834366' - -from alembic import op -import sqlalchemy as sa - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/website/scriptfan/models/__init__.py b/website/scriptfan/models/__init__.py index c28128d..6481a21 100644 --- a/website/scriptfan/models/__init__.py +++ b/website/scriptfan/models/__init__.py @@ -26,14 +26,12 @@ class UserInfo(db.Model): id = db.Column(db.Integer, primary_key=True) motoo = db.Column(db.String(255)) # 座右铭 introduction = db.Column(db.Text) # 个人简介 - phone = db.Column(db.String(15), unique=True, nullable=True) # 手机号码 - phone_status = db.Column(db.Integer, nullable=True) # 手机可见度: 0-不公开 1-公开 2-向成员公开 - photo = db.Column(db.String(255), nullable=True) # 存一张照片,既然有线下的聚会的,总得认得人才行 + phone = db.Column(db.String(15)) # 手机号码 + phone_status = db.Column(db.Integer) # 手机可见度: 0-不公开 1-公开 2-向成员公开 + photo = db.Column(db.String(255)) # 存一张照片,既然有线下的聚会的,总得认得人才行 user = db.relationship('User', backref='info', uselist=False) - def __repr__(self): - return "" % self.user.id class User(db.Model): """ From 2e380d0319ed3482dbee5d7c48a8d8b0347e3a41 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sat, 24 Nov 2012 00:56:36 +0800 Subject: [PATCH 006/119] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E3=80=81=E9=82=AE=E7=AE=B1=E3=80=81=E7=85=A7=E7=89=87=E7=9A=84?= =?UTF-8?q?=E9=9A=90=E7=A7=81=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../107b4efd0f7c_rename_security_to_p.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 website/migrate/versions/107b4efd0f7c_rename_security_to_p.py diff --git a/website/migrate/versions/107b4efd0f7c_rename_security_to_p.py b/website/migrate/versions/107b4efd0f7c_rename_security_to_p.py new file mode 100644 index 0000000..9a69bee --- /dev/null +++ b/website/migrate/versions/107b4efd0f7c_rename_security_to_p.py @@ -0,0 +1,31 @@ +"""rename_security_to_privacy + +Revision ID: 107b4efd0f7c +Revises: 30cb49648d54 +Create Date: 2012-11-24 00:50:50.629688 + +""" + +# revision identifiers, used by Alembic. +revision = '107b4efd0f7c' +down_revision = '30cb49648d54' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.alter_column('users', 'email_security', name='email_privacy', + existing_type=sa.Integer) + op.alter_column('users', 'phone_security', name='phone_privacy', + existing_type=sa.Integer) + op.alter_column('users', 'photo_security', name='photo_privacy', + existing_type=sa.Integer) + +def downgrade(): + op.alter_column('users', 'email_privacy', name='email_security', + existing_type=sa.Integer) + op.alter_column('users', 'phone_privacy', name='phone_security', + existing_type=sa.Integer) + op.alter_column('users', 'photo_privacy', name='photo_security', + existing_type=sa.Integer) From 4b39534f9376a4e1210bb1454654eb2e6c26e6f8 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Mon, 26 Nov 2012 23:03:50 +0800 Subject: [PATCH 007/119] =?UTF-8?q?=E9=87=8D=E5=BB=BAuser=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重建user模型 2. 添加用户权限控制字段 3. 完善用户Model中的注释 --- .../versions/24c171278ca6_add_privilege.py | 21 +++++ website/scriptfan/forms/user.py | 7 +- website/scriptfan/models/__init__.py | 82 +++++++++---------- 3 files changed, 67 insertions(+), 43 deletions(-) create mode 100644 website/migrate/versions/24c171278ca6_add_privilege.py diff --git a/website/migrate/versions/24c171278ca6_add_privilege.py b/website/migrate/versions/24c171278ca6_add_privilege.py new file mode 100644 index 0000000..4e3fcc6 --- /dev/null +++ b/website/migrate/versions/24c171278ca6_add_privilege.py @@ -0,0 +1,21 @@ +"""add privilege + +Revision ID: 24c171278ca6 +Revises: 107b4efd0f7c +Create Date: 2012-11-26 22:34:28.876708 + +""" + +# revision identifiers, used by Alembic. +revision = '24c171278ca6' +down_revision = '107b4efd0f7c' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('users', sa.Column('privilege', sa.Integer)) + +def downgrade(): + op.drop_column('users', 'privilege') diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index 841c8b5..823beb1 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -4,8 +4,9 @@ ~~~~~~~~~~~~~~~~~~ 定义用户相关页面所用到的表单 """ + from flask import session -from scriptfan.models import (get_user, User, UserInfo, UserOpenID) +from scriptfan.models import get_user, User, UserOpenID from scriptfan.forms import RedirectForm from flask.ext import wtf from flask.ext.login import current_user @@ -40,6 +41,7 @@ def validate(self): return len(self.errors) == 0 + class SignupForm(wtf.Form): email = wtf.TextField('email', validators=[ wtf.Required(message=u'请填写电子邮件'), @@ -70,10 +72,10 @@ def validate(self): self.user = User(email=self.email.data, nickname=self.nickname.data, openids=[ UserOpenID(provider=session['openid_provider'], openid=session['current_openid'])]) self.user.set_password(self.password.data) - self.user.info = UserInfo() return len(self.errors) == 0 + class ProfileForm(wtf.Form): nickname = wtf.TextField('nickname', validators=[wtf.Required(message=u'请填写昵称')]) slug = wtf.TextField('slug', validators=[ @@ -92,6 +94,7 @@ def __init__(self, *args, **kargs): wtf.Form.__init__(self, *args, **kargs) self.user = None + class EditPassForm(RedirectForm): old_password= wtf.PasswordField(u'当前密码', validators=[wtf.Required(message=u'请提供当前密码')]) password = wtf.PasswordField(u'新密码', validators=[ \ diff --git a/website/scriptfan/models/__init__.py b/website/scriptfan/models/__init__.py index 6481a21..47bb698 100644 --- a/website/scriptfan/models/__init__.py +++ b/website/scriptfan/models/__init__.py @@ -17,43 +17,43 @@ def get_user(slug=None, user_id=None, email=None): return user -class UserInfo(db.Model): - """ - 用户信息表 - """ - __tablename__ = 'user_info' - - id = db.Column(db.Integer, primary_key=True) - motoo = db.Column(db.String(255)) # 座右铭 - introduction = db.Column(db.Text) # 个人简介 - phone = db.Column(db.String(15)) # 手机号码 - phone_status = db.Column(db.Integer) # 手机可见度: 0-不公开 1-公开 2-向成员公开 - photo = db.Column(db.String(255)) # 存一张照片,既然有线下的聚会的,总得认得人才行 - - user = db.relationship('User', backref='info', uselist=False) - - class User(db.Model): - """ - 用户表 - 修改email地址时需要经过验证 - """ __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(50), unique=True, nullable=False) # 登陆使用的 - email_status = db.Column(db.Integer, nullable=True, default=0) # 邮箱可见度: 0-不公开 1-公开 2-向成员公开 - nickname = db.Column(db.String(50), unique=True, nullable=False) # 昵称, 显示时用的 - password = db.Column(db.String(50), nullable=True) # 密码 - is_email_verified = db.Column(db.Boolean, nullable=False, default=True) - slug = db.Column(db.String(50), nullable=True) # 用户页面 - created_time = db.Column(db.DateTime, nullable=False, default=datetime.now) # 用户注册时间 - modified_time = db.Column(db.DateTime, nullable=False, default=datetime.now) # 用户更新时间 - last_login_time = db.Column(db.DateTime) # 最后一次登陆时间 - privilege = db.Column(db.Integer, default=3) # 权重:3-普通用户 4-管理员 - - user_info_id = db.Column(db.Integer, db.ForeignKey('user_info.id'), nullable=False) - + #: 用户页面的地址后缀,比如 http://scriptfan.com/profile/greatghoul 中的 greatghoul + # 如果要填写,不能重复,因为 slug 要能够唯一标识一个用户 + slug = db.Column(db.String(255), unique=True) + #: 用户的昵称,昵称只是用户的称呼,可能重复 + nickname = db.Column(db.String(255), unique=True, nullable=False) + #: 用户的密码(经过MD5加密的) + password = db.Column(db.String(255)) + #: 邮件地址,可以不填,如果填写,不能重复 + email = db.Column(db.String(255), unique=True) + #: 邮件的隐私度,0为不公开,1为对会员公司,2为完全公开 + email_privacy = db.Column(db.Integer, default=0) + #: 用户的联系电话,可以不填写, 但如果填写的话,不能重复 + phone = db.Column(db.String(255), unique=True) + #: 电话的隐私度,参考 `email_privacy` + phone_privacy = db.Column(db.Integer, default=0) + #: 用户的照片,考虑到是线下社区,所以留张照片能够方便大家互相认识,可以不上传 + photo = db.Column(db.String(255)) + #: 照片的隐私度,参考 `email_privacy` + photo_privacy = db.Column(db.Integer, default=0) + #: 一句话的座右铭 + motoo = db.Column(db.String(255)) + #: 用户的自己介绍,会通过 markdown 转换成 html 文本 + intro = db.Column(db.Text) + #: 上次登陆的时间 + login_time = db.Column(db.DateTime) + #: 注册时间 + created_time = db.Column(db.DateTime, default=datetime.now) + #: 上次更新资料的时间 + updated_time = db.Column(db.DateTime, default=datetime.now) + #: 简单的权限控制,控制级别:3-普通用户 4-管理员 (目前就这么简单,后面再讨论) + privilege = db.Column(db.Integer, default=3) + + #: 用户 openid 的绑定列表 openids = db.relationship('UserOpenID', backref=db.backref('user')) def __repr__(self): @@ -76,18 +76,18 @@ def get_avatar_url(self, size=20): return url_tpl % (md5(self.email), size, request.url_root, url_for('static', filename='images/avatars/default.png')) class UserOpenID(db.Model): - """ - 用户绑定OpenID的表 - 一个用户可以对应多个OpenID - """ + """ OpenID绑定表 """ __tablename__ = 'user_openids' id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) # openid关联的用户 - openid = db.Column(db.String(255), nullable=False, unique=True) # 记录的 openid, 不能重复 - provider = db.Column(db.String(50), nullable=False) # openid的提供商,比如 google + #: openid 关联的用户 + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + #: openid 授权的值 + openid = db.Column(db.String(255), nullable=False, unique=True) + #: opendid 提供商, 例如 google + provider = db.Column(db.String(50), nullable=False) -class Resource(db.Model): +class Resource(db.Model): """ 资源表 汇集图片、视频、演示文稿等资源, 用于嵌入活动中 From 1a86d51d2c4c0339a0e1f9fc51c6508e0378e111 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Mon, 26 Nov 2012 23:31:56 +0800 Subject: [PATCH 008/119] =?UTF-8?q?=E5=B0=86=20get=5Fuser=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E8=BD=AC=E6=8D=A2=E5=88=B0=20User=20=E4=B8=AD?= =?UTF-8?q?=EF=BC=8C=E8=BD=AC=E6=8D=A2=E4=B8=BA=E7=B1=BB=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 将 `get_user` 拆分为 `User.get_by_email()`, 'User.get_by_slug()' 两个独立的方法 2. 简化用户相关表单 --- website/scriptfan/forms/user.py | 53 ++++++++++------------------ website/scriptfan/models/__init__.py | 20 +++++------ 2 files changed, 27 insertions(+), 46 deletions(-) diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index 823beb1..7c42293 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -1,4 +1,5 @@ #-*- coding: utf-8 -*- + """ forms/user.py ~~~~~~~~~~~~~~~~~~ @@ -6,74 +7,57 @@ """ from flask import session -from scriptfan.models import get_user, User, UserOpenID -from scriptfan.forms import RedirectForm from flask.ext import wtf from flask.ext.login import current_user -class SigninForm(wtf.Form): +from scriptfan.models import User, UserOpenID +from scriptfan.forms import RedirectForm + +class SigninForm(RedirectForm): email = wtf.TextField('email', validators=[ wtf.Required(message=u'请填写电子邮件'), wtf.Email(message=u'无效的电子邮件')]) password = wtf.PasswordField('password', validators=[ - wtf.Required(message=u'请填写密码'), - wtf.Length(min=5, max=20, message=u'密应应为5到20位字符')]) - next = wtf.HiddenField('next') + wtf.Required(message=u'请填写密码')]) remember = wtf.BooleanField('remember') openid_identifier = wtf.HiddenField('openid_identifier') openid_provider = wtf.HiddenField('openid_provider') - def __init__(self, *args, **kargs): - wtf.Form.__init__(self, *args, **kargs) - self.user = None - def validate(self): # 验证邮箱是否注册 if wtf.Form.validate(self): - user = get_user(email=self.email.data) + user = User.get_by_email(self.email.data) if not user: - self.email.errors.append(u'该邮箱尚未在本站注册') + self.email.errors.append(u'邮箱未注册') elif not user.check_password(self.password.data): self.password.errors.append(u'密码错误') - else: - self.user = user - return len(self.errors) == 0 + return not self.errors -class SignupForm(wtf.Form): +class SignupForm(RedirectForm): email = wtf.TextField('email', validators=[ wtf.Required(message=u'请填写电子邮件'), wtf.Email(message=u'无效的电子邮件')]) nickname = wtf.TextField('nickname', validators=[ - wtf.Required(message=u'请填写昵称'), - wtf.Length(min=2, max=20, message=u'昵称应为2到20字符')]) + wtf.Required(message=u'请填写昵称')]) password = wtf.PasswordField('password', validators=[ - wtf.Required(message=u'请填写密码'), - wtf.Length(min=5, max=20, message=u'密码应为5到20位字符')]) + wtf.Required(message=u'请填写密码')]) repassword = wtf.PasswordField('repassword', validators=[ - wtf.Required(message=u'请填写确认密码'), + wtf.Required(message=u'再次填写密码'), wtf.EqualTo('password', message=u'两次输入的密码不一致')]) - next = wtf.HiddenField('next') - - def __init__(self, *args, **kargs): - wtf.Form.__init__(self, *args, **kargs) - self.user = None def validate(self): - wtf.Form.validate(self) - - # 验证邮箱是否注册 if not self.email.errors: - user = get_user(email=self.email.data) + user = User.get_by_email(self.email.data) user and self.email.errors.append(u'该邮箱已被注册') - self.user = User(email=self.email.data, nickname=self.nickname.data, openids=[ - UserOpenID(provider=session['openid_provider'], openid=session['current_openid'])]) - self.user.set_password(self.password.data) + # self.user = User(email=self.email.data, nickname=self.nickname.data, openids=[ + # UserOpenID(provider=session['openid_provider'], openid=session['current_openid'])]) + # self.user.set_password(self.password.data) - return len(self.errors) == 0 + return not self.errors class ProfileForm(wtf.Form): @@ -107,3 +91,4 @@ class EditPassForm(RedirectForm): def validate_old_password(form, field): if not current_user.user.check_password(field.data): raise wtf.ValidationError(u'提供的原始密码不正确') + diff --git a/website/scriptfan/models/__init__.py b/website/scriptfan/models/__init__.py index 47bb698..7f2b6dc 100644 --- a/website/scriptfan/models/__init__.py +++ b/website/scriptfan/models/__init__.py @@ -5,18 +5,6 @@ from scriptfan.extensions import db from scriptfan.utils.functions import md5 -def get_user(slug=None, user_id=None, email=None): - user = None - - if email: - user = User.query.filter_by(email=email).first() - elif slug: - user = User.query.filter_by(slug=slug).first() - elif user_id: - user = User.query.filter_by(id=user_id).first() - - return user - class User(db.Model): __tablename__ = 'users' @@ -65,6 +53,14 @@ def set_password(self, password): def check_password(self, password): return self.password == md5(password) + @classmethod + def get_by_email(email): + return User.query.filter_by(email=email).first() + + @classmethod + def get_by_slug(slug): + return User.query.filter_by(slug=slug).first() + @property def url(self): if self.slug: From 281cf69e1885bac9a109ae938f3f0292851517b9 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 28 Nov 2012 23:45:45 +0800 Subject: [PATCH 009/119] =?UTF-8?q?=E9=87=8D=E5=86=99=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 优化注册页面样式 2. 优化表单验证 3. 修正 User.get_by_email, User.get_by_slug 的语法错误 4. 完成用户注册功能 --- website/scriptfan/forms/user.py | 21 +++--- website/scriptfan/models/__init__.py | 4 +- website/scriptfan/static/css/styles.css | 53 ++++++++------- website/scriptfan/templates/user/signup.html | 70 ++++++++++---------- website/scriptfan/views/user.py | 17 ++--- 5 files changed, 83 insertions(+), 82 deletions(-) diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index 7c42293..89f2858 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -6,11 +6,10 @@ 定义用户相关页面所用到的表单 """ -from flask import session from flask.ext import wtf from flask.ext.login import current_user -from scriptfan.models import User, UserOpenID +from scriptfan.models import User from scriptfan.forms import RedirectForm class SigninForm(RedirectForm): @@ -42,24 +41,20 @@ class SignupForm(RedirectForm): wtf.Email(message=u'无效的电子邮件')]) nickname = wtf.TextField('nickname', validators=[ wtf.Required(message=u'请填写昵称')]) - password = wtf.PasswordField('password', validators=[ + password1 = wtf.PasswordField('password1', validators=[ wtf.Required(message=u'请填写密码')]) - repassword = wtf.PasswordField('repassword', validators=[ + password2 = wtf.PasswordField('password2', validators=[ wtf.Required(message=u'再次填写密码'), - wtf.EqualTo('password', message=u'两次输入的密码不一致')]) - - def validate(self): - if not self.email.errors: - user = User.get_by_email(self.email.data) - user and self.email.errors.append(u'该邮箱已被注册') + wtf.EqualTo('password1', message=u'两次输入的密码不一致')]) + def validate_email(form, field): + if User.get_by_email(field.data): + raise wtf.ValidationError(u'该邮箱已被注册') + print form.errors # self.user = User(email=self.email.data, nickname=self.nickname.data, openids=[ # UserOpenID(provider=session['openid_provider'], openid=session['current_openid'])]) # self.user.set_password(self.password.data) - return not self.errors - - class ProfileForm(wtf.Form): nickname = wtf.TextField('nickname', validators=[wtf.Required(message=u'请填写昵称')]) slug = wtf.TextField('slug', validators=[ diff --git a/website/scriptfan/models/__init__.py b/website/scriptfan/models/__init__.py index 7f2b6dc..20de242 100644 --- a/website/scriptfan/models/__init__.py +++ b/website/scriptfan/models/__init__.py @@ -54,11 +54,11 @@ def check_password(self, password): return self.password == md5(password) @classmethod - def get_by_email(email): + def get_by_email(cls, email): return User.query.filter_by(email=email).first() @classmethod - def get_by_slug(slug): + def get_by_slug(cls, slug): return User.query.filter_by(slug=slug).first() @property diff --git a/website/scriptfan/static/css/styles.css b/website/scriptfan/static/css/styles.css index 426190f..0bd6bc3 100644 --- a/website/scriptfan/static/css/styles.css +++ b/website/scriptfan/static/css/styles.css @@ -12,31 +12,36 @@ body > div.navbar > div.navbar-inner > div.container > div > form > input { height:26px; } -.container > .content { - -webkit-box-shadow: inset 0 1px 0 white,0 2px 5px #eee; - -moz-box-shadow: inset 0 1px 0 white,0 2px 5px #eee; - box-shadow: inset 0 1px 0 white,0 2px 5px #eee; - border: 1px solid #e6e6e6; - border-bottom-color: #d6d6d6; - background-color: #ffffff; - padding: 10px; - min-height: 38px; - border-radius: 6px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - -moz-background-clip: padding; - -webkit-background-clip: padding-box; - background-clip: padding-box; -} -.content form, -.content .form-actions { - margin-bottom: 0; +.content { + background-color: white; + padding: 20px; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, .15); + -moz-box-shadow: 0 2px 4px rgba(0,0,0,.15); + box-shadow: 0 2px 4px rgba(0, 0, 0, .15); } -.content .page-header { - margin-top: 0; - margin-bottom: 20px; - padding-bottom: 0; +.form-horizontal { + margin-bottom: 0px; +} + +.form-actions { + margin: auto -20px -20px -20px; +} + +.page-header { + background-color: whiteSmoke; + padding: 20px 20px 10px; + margin: -20px -20px 20px; + height: 40px; +} + +.page-header h1 { + margin: 0; + font-size: 30px; + line-height: 1; } .page-header .avatar-large { @@ -45,4 +50,4 @@ body > div.navbar > div.navbar-inner > div.container > div > form > input { width: 110px; height: 110px; background: #eeeeee, -} \ No newline at end of file +} diff --git a/website/scriptfan/templates/user/signup.html b/website/scriptfan/templates/user/signup.html index 6ed18e5..3933732 100644 --- a/website/scriptfan/templates/user/signup.html +++ b/website/scriptfan/templates/user/signup.html @@ -1,44 +1,44 @@ {% extends "base.html" %} {% block title %}注册会员{% endblock %} {% block content %} -

用户注册

+
+ - -
- -
- {{ form.email }} - {{ form.email.errors | first }} + +
+ +
+ {{ form.email }} + {{ form.email | error_text }} +
-
-
- -
- {{ form.nickname }} - {{ form.nickname.errors | first }} +
+ +
+ {{ form.nickname }} + {{ form.nickname | error_text }} +
-
-
- -
- {{ form.password }} - {{ form.password.errors | first }} +
+ +
+ {{ form.password1 }} + {{ form.password1 | error_text }} +
-
-
- -
- {{ form.repassword }} - {{ form.repassword.errors | first }} +
+ +
+ {{ form.password2 }} + {{ form.password2 | error_text }} +
-
- {% if session.current_openid %} -
- 正在通过 {{ session.openid_provider }} OpenID 注册,填写密码以完成注册! -
- {% endif %} -
- -
- +
+ +   或者直接 使用Google登陆 +
+ +
{% endblock %} diff --git a/website/scriptfan/views/user.py b/website/scriptfan/views/user.py index 8145160..cf018d7 100644 --- a/website/scriptfan/views/user.py +++ b/website/scriptfan/views/user.py @@ -9,8 +9,8 @@ from flask.ext import login from flask.ext.login import current_user from scriptfan.extensions import db, oid, login_manager -from scriptfan.models import (User, UserOpenID) -from scriptfan.forms.user import (SignupForm, SigninForm, ProfileForm, EditPassForm) +from scriptfan.models import User, UserOpenID +from scriptfan.forms.user import SignupForm, SigninForm, ProfileForm, EditPassForm # import re @@ -79,13 +79,13 @@ def signup(): if current_user.is_authenticated(): return redirect(url_for('user.profile')) - app.logger.info('request.form: ' + repr(request.values)) - form = SignupForm(request.values, csrf_enabled=False) - app.logger.info('>>> Signup user: ' + repr(dict(form.data, password=''))) - + form = SignupForm(csrf_enabled=False) if form.validate_on_submit(): - db.session.add(form.user) - db.session.commit() + user = User() + form.populate_obj(user) + user.set_password(form.password1.data) + db.session.add(user) + flash(u'注册成功', 'success') return redirect(url_for('user.signin')) else: return render_template('user/signup.html', form=form) @@ -164,3 +164,4 @@ def signout(): login.logout_user() del session['current_openid'] return redirect(url_for('site.index')) + From ea94f50d030831f90065b63ebe11911d7c2a9457 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Fri, 30 Nov 2012 21:57:15 +0800 Subject: [PATCH 010/119] =?UTF-8?q?=E9=87=8D=E5=86=99=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=99=BB=E9=99=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重写用户登陆 2. 修正用户注销页面URL命名错误 --- website/scriptfan/__init__.py | 1 + website/scriptfan/forms/user.py | 19 +++---- website/scriptfan/models/__init__.py | 2 +- website/scriptfan/templates/user/signin.html | 56 ++++++++++---------- website/scriptfan/templates/user/signup.html | 13 +++-- website/scriptfan/views/user.py | 29 ++++------ 6 files changed, 62 insertions(+), 58 deletions(-) diff --git a/website/scriptfan/__init__.py b/website/scriptfan/__init__.py index f17c62a..d36ec24 100644 --- a/website/scriptfan/__init__.py +++ b/website/scriptfan/__init__.py @@ -11,6 +11,7 @@ app = Flask(__name__, instance_path=instance_path, instance_relative_config=True) def config_app(app, config): + app.debug_log_format = '[%(levelname)s] %(message)s' logger.info('Setting up application...') app.config.from_pyfile(config) db.init_app(app) diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index 89f2858..9d560d6 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -24,14 +24,16 @@ class SigninForm(RedirectForm): openid_provider = wtf.HiddenField('openid_provider') def validate(self): - # 验证邮箱是否注册 - if wtf.Form.validate(self): - user = User.get_by_email(self.email.data) - if not user: - self.email.errors.append(u'邮箱未注册') - elif not user.check_password(self.password.data): - self.password.errors.append(u'密码错误') - + if self.errors: return False + + user = User.get_by_email(self.email.data) + if not user: + self.email.errors.append(u'该邮箱未注册') + elif not user.check_password(self.password.data): + self.password.errors.append(u'密码错误') + else: + self.user = user + return not self.errors @@ -50,7 +52,6 @@ class SignupForm(RedirectForm): def validate_email(form, field): if User.get_by_email(field.data): raise wtf.ValidationError(u'该邮箱已被注册') - print form.errors # self.user = User(email=self.email.data, nickname=self.nickname.data, openids=[ # UserOpenID(provider=session['openid_provider'], openid=session['current_openid'])]) # self.user.set_password(self.password.data) diff --git a/website/scriptfan/models/__init__.py b/website/scriptfan/models/__init__.py index 20de242..e00407f 100644 --- a/website/scriptfan/models/__init__.py +++ b/website/scriptfan/models/__init__.py @@ -45,7 +45,7 @@ class User(db.Model): openids = db.relationship('UserOpenID', backref=db.backref('user')) def __repr__(self): - return "" % (self.nickname, self.email) + return u'' % (self.nickname, self.email) def set_password(self, password): self.password = md5(password) diff --git a/website/scriptfan/templates/user/signin.html b/website/scriptfan/templates/user/signin.html index c26575d..8c6c9f0 100644 --- a/website/scriptfan/templates/user/signin.html +++ b/website/scriptfan/templates/user/signin.html @@ -1,4 +1,5 @@ {% extends "base.html" %} + {% block title %}会员登陆{% endblock %} {% block scripts %} @@ -6,34 +7,35 @@ {% endblock %} {% block content %} -

用户登陆

-
- {{ form.hidden_tag() }} -
- -
- {{ form.email }} - {{ form.email.errors | first }} -
+
+ -
- -
- {{ form.password }} - {{ form.password.errors | first }} + + + {{ form.hidden_tag() }} +
+ +
+ {{ form.email }} + {{ form.email | error_text }} +
-
- {% if error %} -
- 错误: {{ error }} +
+ +
+ {{ form.password }} + {{ form.password | error_text }} +
- {% endif %} -
-    - -
使用 Gmail帐户 登陆
-
- +
+    + +
使用 Gmail帐户 登陆
+
+ +
{% endblock %} + diff --git a/website/scriptfan/templates/user/signup.html b/website/scriptfan/templates/user/signup.html index 3933732..c62273f 100644 --- a/website/scriptfan/templates/user/signup.html +++ b/website/scriptfan/templates/user/signup.html @@ -1,5 +1,11 @@ {% extends "base.html" %} + {% block title %}注册会员{% endblock %} + +{% block scripts %} +{{ t.js('lib/jquery-openid.js') }} +{% endblock %} + {% block content %}
-   或者直接 使用Google登陆 +   或者直接 使用Google登陆
- -
+ +
{% endblock %} + diff --git a/website/scriptfan/views/user.py b/website/scriptfan/views/user.py index cf018d7..78e90e9 100644 --- a/website/scriptfan/views/user.py +++ b/website/scriptfan/views/user.py @@ -12,8 +12,6 @@ from scriptfan.models import User, UserOpenID from scriptfan.forms.user import SignupForm, SigninForm, ProfileForm, EditPassForm -# import re - userapp = Blueprint("user", __name__) class Anonymous(login.AnonymousUser): @@ -36,27 +34,19 @@ def load_user(user_id): return user and LoginUser(user) or None @userapp.route('/signin/', methods=['GET', 'POST']) -@oid.loginhandler def signin(): if current_user.is_authenticated(): return redirect(url_for('user.profile')) - form = SigninForm(csrf_enabled=False, next=oid.get_next_url()) - app.logger.info('>>> Signin user: ' + repr(dict(form.data, password=''))) + form = SigninForm(csrf_enabled=False) + app.logger.info('* Signin user: %s', form.email.data) - if form.is_submitted() and form.openid_identifier.data: - session['openid_provider'] = form.openid_provider.data - session['openid_identifier'] = form.openid_identifier.data - return oid.try_login(form.openid_identifier.data, ask_for=['email', 'nickname', 'fullname']) - if form.validate_on_submit(): login.login_user(LoginUser(form.user), remember=form.remember) - flash(u'登陆成功') - # 如果指定了 next ,跳转到 next 页面 - # 如果用户注册了 slug ,则跳转到 slug 的profile 页面,否则跳转到 userid 的 profile 页面 - return redirect(form.next.data or url_for('user.profile')) - else: - return render_template('user/signin.html', form=form, openid_error=oid.fetch_error()) + flash(u'登陆成功', 'success') + return form.redirect('user.profile') + return render_template('user/signin.html', form=form) + @oid.after_login def create_or_login(resp): @@ -80,11 +70,13 @@ def signup(): return redirect(url_for('user.profile')) form = SignupForm(csrf_enabled=False) + app.logger.info(u' * Signup with email: %(email)s, nickname: %(nickname)s', form.data) if form.validate_on_submit(): user = User() form.populate_obj(user) user.set_password(form.password1.data) db.session.add(user) + app.logger.info(u'New user added: %s', user) flash(u'注册成功', 'success') return redirect(url_for('user.signin')) else: @@ -158,10 +150,11 @@ def edit_pass(): def editemail(): return 'email' -@userapp.route('/signou/', methods=['GET']) +@userapp.route('/signout/', methods=['GET']) @login.login_required def signout(): login.logout_user() - del session['current_openid'] + if 'current_openid' in session: + del session['current_openid'] return redirect(url_for('site.index')) From b1e3f530f5bb951048abf5b75d5c1065ff674763 Mon Sep 17 00:00:00 2001 From: jinz Date: Sat, 1 Dec 2012 16:07:32 +0800 Subject: [PATCH 011/119] =?UTF-8?q?flaskext.*=E7=9A=84=E5=BD=A2=E5=BC=8F?= =?UTF-8?q?=E5=9C=A8flask0.9=E7=89=88=E6=9C=AC=E4=B8=AD=E4=B8=8D=E8=83=BD?= =?UTF-8?q?=E8=AF=86=E5=88=AB=EF=BC=8C=E6=94=B9=E4=B8=BAflask.ext.*?= =?UTF-8?q?=E5=BD=A2=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/manage.py | 2 +- website/scriptfan/extensions/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/manage.py b/website/manage.py index cad7164..f3e10ba 100644 --- a/website/manage.py +++ b/website/manage.py @@ -6,7 +6,7 @@ import logging logging.basicConfig(level=logging.INFO) -from flaskext.script import Manager, Shell +from flask.ext.script import Manager, Shell from scriptfan import app, db, oid, config_app, dispatch_handlers, dispatch_apps manager = Manager(app, with_default_commands=False) diff --git a/website/scriptfan/extensions/__init__.py b/website/scriptfan/extensions/__init__.py index 044307d..f651654 100644 --- a/website/scriptfan/extensions/__init__.py +++ b/website/scriptfan/extensions/__init__.py @@ -1,5 +1,5 @@ #-*- coding: utf-8 -*- -from flaskext.openid import OpenID +from flask.ext.openid import OpenID from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.login import LoginManager From bd4a01120863cb3f3780b9949ddeeb033b26a66a Mon Sep 17 00:00:00 2001 From: jinz Date: Sat, 1 Dec 2012 18:52:41 +0800 Subject: [PATCH 012/119] =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E6=96=B0=E7=9A=84use?= =?UTF-8?q?r=E8=A1=A8=EF=BC=8C=E9=87=8D=E5=86=99=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=B5=84=E6=96=99=201.=E4=BF=AE=E5=A4=8D=E4=BA=86=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=B5=84=E6=96=99=E6=98=BE=E7=A4=BA=EF=BC=8C=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=B5=84=E6=96=99=E4=BF=AE=E6=94=B9=202.=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E4=BA=86=E9=A6=96=E9=A1=B5=E6=A0=87=E9=A2=98=EF=BC=8C?= =?UTF-8?q?"=E9=A6=96=E9=9D=A2"-"=E9=A6=96=E9=A1=B5"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/forms/user.py | 2 +- website/scriptfan/templates/index.html | 2 +- website/scriptfan/templates/user/edit.html | 4 ++-- website/scriptfan/templates/user/profile.html | 14 +++++++------- website/scriptfan/views/user.py | 16 +++++++++------- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index 9d560d6..dbf2f81 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -62,7 +62,7 @@ class ProfileForm(wtf.Form): wtf.Regexp(regex=r'^([a-zA-Z][a-zA-Z0-9_-]{4,23})?$', message=u'长度应为5~24位,仅能包含数字、英文字母及下划线(_)和减号(-),并且需要以字母开头')]) phone = wtf.TextField('phone', validators=[ wtf.Regexp(regex=r'^(1\d{10})?$', message=u'请输入有效的手机号码')]) - phone_status = wtf.RadioField('phone_status', choices=[ + phone_privacy = wtf.RadioField('phone_privacy', choices=[ ('0', u'不公开'), ('1', u'公开'), ('2', u'仅向会员公开')], default='0') # photo = db.Column(db.String(255), nullable=True) # 存一张照片,既然有线下的聚会的,总得认得人才行 motoo = wtf.TextAreaField('motoo', validators=[ diff --git a/website/scriptfan/templates/index.html b/website/scriptfan/templates/index.html index 9f8edc2..0a8247e 100644 --- a/website/scriptfan/templates/index.html +++ b/website/scriptfan/templates/index.html @@ -1,3 +1,3 @@ {% extends "base.html" %} -{% block title %}首面{% endblock %} +{% block title %}首页{% endblock %} diff --git a/website/scriptfan/templates/user/edit.html b/website/scriptfan/templates/user/edit.html index b38185c..994efd7 100644 --- a/website/scriptfan/templates/user/edit.html +++ b/website/scriptfan/templates/user/edit.html @@ -24,7 +24,7 @@

修改用户资料

- greatghoul@gmail.com + {{ form.user_email }} 修改邮箱
@@ -64,7 +64,7 @@

修改用户资料

- {% for radio in form.phone_status %} + {% for radio in form.phone_privacy %} diff --git a/website/scriptfan/templates/user/profile.html b/website/scriptfan/templates/user/profile.html index fb162c4..0932534 100644 --- a/website/scriptfan/templates/user/profile.html +++ b/website/scriptfan/templates/user/profile.html @@ -17,16 +17,16 @@

{{ user.nickname }} 的资料
电子邮件:{{ user.email }}
{% if user.id == current_user.user.id %} -
手机号码:{{ user.info.phone }}
+
手机号码:{{ user.phone }}
{% elif not current_user.is_anonymous() %} - {% if user.info.phone_status in (1, 2) %} -
手机号码:{{ user.info.phone }}
+ {% if user.phone_privacy in (1, 2) %} +
手机号码:{{ user.phone }}
{% endif %} - {% elif user.info.phone_status == 1 %} -
手机号码:{{ user.info.phone }}
+ {% elif user.phone_privacy == 1 %} +
手机号码:{{ user.phone }}
{% endif %} -
座右铭:{{ user.info.motoo }}
-
个人介绍:{{ user.info.introduction }}
+
座右铭:{{ user.motoo }}
+
个人介绍:{{ user.intro }}
注册日期:{{ user.created_time | dateformat }}

diff --git a/website/scriptfan/views/user.py b/website/scriptfan/views/user.py index 78e90e9..1a32216 100644 --- a/website/scriptfan/views/user.py +++ b/website/scriptfan/views/user.py @@ -106,10 +106,10 @@ def edit(): try: user = current_user.user user.nickname = form.data['nickname'] - user.info.phone = form.data['phone'] - user.info.phone_status = form.data['phone_status'] - user.info.motoo = form.data['motoo'] - user.info.introduction = form.data['introduction'] + user.phone = form.data['phone'] + user.phone_privacy = form.data['phone_privacy'] + user.motoo = form.data['motoo'] + user.intro = form.data['introduction'] if form.data['slug']: user.slug = form.data.get('slug') return jsonify(success=True, messages=dict(success=u'用户资料更新成功')) @@ -123,9 +123,11 @@ def edit(): user = current_user.user form.nickname.data = user.nickname form.slug.data = user.slug - form.phone.data = user.info.phone - form.motoo.data = user.info.motoo - form.introduction.data = user.info.introduction + form.phone.data = user.phone + form.phone_privacy.data = unicode(user.phone_privacy) + form.motoo.data = user.motoo + form.introduction.data = user.intro + form.user_email = user.email return render_template('user/edit.html', form=form) # TODO 处理更新用户资料的请求 From 2474061656ad18947cee011156d0a11477cba9de Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sat, 1 Dec 2012 20:27:40 +0800 Subject: [PATCH 013/119] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=BD=BF=E7=94=A8=20?= =?UTF-8?q?Flask=20>=3D=200.9=20=E7=9A=84=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b895877..2d4c2db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -flask >= 0.8 +flask >= 0.9 flask-script >= 0.3.3 flask-sqlalchemy >= 0.16 flask-login >= 0.1.3 From 2373ea6d4d905fcc3f77265a8bb6b3328092ac70 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sat, 1 Dec 2012 21:13:05 +0800 Subject: [PATCH 014/119] =?UTF-8?q?=E6=94=B9=E8=BF=9B=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 废弃 ajax 调用,直接使用传统方式提交表单 2. 使用标准的 form.populate_obj(obj=instance), form.process(obj) 来进行 from 到 model 值的传递 3. 修正用户资料编辑页面标题的样式 --- website/scriptfan/forms/user.py | 10 ++--- website/scriptfan/static/js/user-edit.js | 11 ----- website/scriptfan/templates/user/edit.html | 17 ++++---- website/scriptfan/views/user.py | 47 ++++++---------------- 4 files changed, 25 insertions(+), 60 deletions(-) diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index dbf2f81..487c007 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -56,8 +56,9 @@ def validate_email(form, field): # UserOpenID(provider=session['openid_provider'], openid=session['current_openid'])]) # self.user.set_password(self.password.data) -class ProfileForm(wtf.Form): +class ProfileForm(RedirectForm): nickname = wtf.TextField('nickname', validators=[wtf.Required(message=u'请填写昵称')]) + # FIXME: 验证 slug 是否已经被占用 slug = wtf.TextField('slug', validators=[ wtf.Regexp(regex=r'^([a-zA-Z][a-zA-Z0-9_-]{4,23})?$', message=u'长度应为5~24位,仅能包含数字、英文字母及下划线(_)和减号(-),并且需要以字母开头')]) phone = wtf.TextField('phone', validators=[ @@ -67,14 +68,9 @@ class ProfileForm(wtf.Form): # photo = db.Column(db.String(255), nullable=True) # 存一张照片,既然有线下的聚会的,总得认得人才行 motoo = wtf.TextAreaField('motoo', validators=[ wtf.Length(min=0, max=255, message=u'座右铭最多为255个字符')]) - introduction = wtf.TextAreaField('introduction', validators=[ + intro = wtf.TextAreaField('introduction', validators=[ wtf.Length(min=0, max=3000, message=u'个人介绍最多为3000个字')]) - def __init__(self, *args, **kargs): - wtf.Form.__init__(self, *args, **kargs) - self.user = None - - class EditPassForm(RedirectForm): old_password= wtf.PasswordField(u'当前密码', validators=[wtf.Required(message=u'请提供当前密码')]) password = wtf.PasswordField(u'新密码', validators=[ \ diff --git a/website/scriptfan/static/js/user-edit.js b/website/scriptfan/static/js/user-edit.js index 86808bf..e69de29 100644 --- a/website/scriptfan/static/js/user-edit.js +++ b/website/scriptfan/static/js/user-edit.js @@ -1,11 +0,0 @@ -/** - * 用户资料修改 - * - * @author greatghoul - */ -$(function() { - var $form = $('#form-user-edit'); - var $submit = $form.find(':submit'); - - $form.bform(); -}); diff --git a/website/scriptfan/templates/user/edit.html b/website/scriptfan/templates/user/edit.html index 994efd7..eb5c819 100644 --- a/website/scriptfan/templates/user/edit.html +++ b/website/scriptfan/templates/user/edit.html @@ -8,16 +8,16 @@ {% block content %}
-
+ {{ form.hidden_tag() }} -
+
{{ form.nickname }} - + {{ form.nickname | error_text }}
@@ -37,7 +37,7 @@

修改用户资料

-
+
{% if form.slug.data %} @@ -46,6 +46,7 @@

修改用户资料

{% else %}
{{ form.slug(placeholder='user-slug') }} +
设定你的个性域名,以便通过 {{ request.url_root }}profile/yourname 访问。
@@ -54,11 +55,11 @@

修改用户资料

-
+
{{ form.phone }} - + {{ form.phone | error_text }}
@@ -83,7 +84,7 @@

修改用户资料

- {{ form.introduction }} + {{ form.intro }}
diff --git a/website/scriptfan/views/user.py b/website/scriptfan/views/user.py index 1a32216..e1d9962 100644 --- a/website/scriptfan/views/user.py +++ b/website/scriptfan/views/user.py @@ -1,9 +1,6 @@ #!/usr/bin/env python #-*-coding:utf-8-*- -import logging -logger = logging.getLogger(__name__) - -from flask import Blueprint, request, session, url_for, redirect, jsonify, abort +from flask import Blueprint, session, url_for, redirect, abort from flask import render_template, flash from flask import current_app as app from flask.ext import login @@ -99,36 +96,18 @@ def profile(slug_or_id=None): @login.login_required def edit(): form = ProfileForm(csrf_enabled=False) - if form.is_submitted(): - logger.info('Updating user information...') - success = form.validate_on_submit() - if success: - try: - user = current_user.user - user.nickname = form.data['nickname'] - user.phone = form.data['phone'] - user.phone_privacy = form.data['phone_privacy'] - user.motoo = form.data['motoo'] - user.intro = form.data['introduction'] - if form.data['slug']: - user.slug = form.data.get('slug') - return jsonify(success=True, messages=dict(success=u'用户资料更新成功')) - except Exception as e: - return jsonify(success=False, messages=dict(error=unicode(e))) - else: - return jsonify(success=False, messages=dict(error=u'用户资料更新失败'), \ - errors=form.errors) - else: - # 如果是编辑用户信息,则使用用户当前信息填充表单 - user = current_user.user - form.nickname.data = user.nickname - form.slug.data = user.slug - form.phone.data = user.phone - form.phone_privacy.data = unicode(user.phone_privacy) - form.motoo.data = user.motoo - form.introduction.data = user.intro - form.user_email = user.email - return render_template('user/edit.html', form=form) + if form.validate_on_submit(): + app.logger.info(' * Updating user information...') + app.logger.info(form.data) + if not form.data['slug']: + form.slug.data = current_user.user.slug + form.populate_obj(current_user.user) + flash(u'用户资料已经更新', 'success') + return form.redirect('user.edit') + + # 如果是编辑用户信息,则使用用户当前信息填充表单 + form.process(obj=current_user.user) + return render_template('user/edit.html', form=form) # TODO 处理更新用户资料的请求 # TODO 用户照片上传 From aafe312bda1f1e29166fa5bd565c1757b655bf97 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sat, 1 Dec 2012 21:24:14 +0800 Subject: [PATCH 015/119] =?UTF-8?q?=E5=8E=BB=E9=99=A4=20manage.py=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E5=85=A8=E5=B1=80=E6=97=A5=E5=BF=97=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/manage.py | 3 --- website/scriptfan/views/user.py | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/website/manage.py b/website/manage.py index f3e10ba..e8be5f2 100644 --- a/website/manage.py +++ b/website/manage.py @@ -3,9 +3,6 @@ import os, sys sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -import logging -logging.basicConfig(level=logging.INFO) - from flask.ext.script import Manager, Shell from scriptfan import app, db, oid, config_app, dispatch_handlers, dispatch_apps diff --git a/website/scriptfan/views/user.py b/website/scriptfan/views/user.py index e1d9962..e01bd38 100644 --- a/website/scriptfan/views/user.py +++ b/website/scriptfan/views/user.py @@ -97,8 +97,8 @@ def profile(slug_or_id=None): def edit(): form = ProfileForm(csrf_enabled=False) if form.validate_on_submit(): - app.logger.info(' * Updating user information...') - app.logger.info(form.data) + app.logger.info(u'* Updating user information...') + app.logger.info(u'Form data: %s', repr(form.data)) if not form.data['slug']: form.slug.data = current_user.user.slug form.populate_obj(current_user.user) From f5dd5a34a68a1347447674af5a1800db8a8aa091 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sun, 2 Dec 2012 00:27:41 +0800 Subject: [PATCH 016/119] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=B5=84=E6=96=99=E4=BF=AE=E6=94=B9=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 添加侧边栏导航, 移除基本信息表单中多余的项 2. 重写修改密码功能 3. 修正登录时,如果邮箱不存在,表单验证异常的问题 4. 重命名父模板文件为 layout.html 5. 更新用户资料修改相关页面的 URL 映射名称 --- website/scriptfan/forms/user.py | 26 ++-- .../templates/activities/create.html | 2 +- .../scriptfan/templates/activities/index.html | 2 +- website/scriptfan/templates/index.html | 2 +- .../templates/{base.html => layout.html} | 4 +- .../scriptfan/templates/user/_sidebar.html | 6 + website/scriptfan/templates/user/edit.html | 117 ------------------ .../scriptfan/templates/user/edit_pass.html | 42 ------- website/scriptfan/templates/user/general.html | 100 +++++++++++++++ website/scriptfan/templates/user/layout.html | 15 +++ .../scriptfan/templates/user/password.html | 44 +++++++ website/scriptfan/templates/user/profile.html | 6 +- website/scriptfan/templates/user/signin.html | 2 +- website/scriptfan/templates/user/signup.html | 2 +- website/scriptfan/views/user.py | 25 ++-- 15 files changed, 200 insertions(+), 195 deletions(-) rename website/scriptfan/templates/{base.html => layout.html} (89%) create mode 100644 website/scriptfan/templates/user/_sidebar.html delete mode 100644 website/scriptfan/templates/user/edit.html delete mode 100644 website/scriptfan/templates/user/edit_pass.html create mode 100644 website/scriptfan/templates/user/general.html create mode 100644 website/scriptfan/templates/user/layout.html create mode 100644 website/scriptfan/templates/user/password.html diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index 487c007..da6658e 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -1,9 +1,9 @@ #-*- coding: utf-8 -*- """ - forms/user.py - ~~~~~~~~~~~~~~~~~~ - 定义用户相关页面所用到的表单 + scriptfan/forms/user.py + ~~~~~~~~~~~~~~~~~~~~~~~ + 定义用户相关页面所用到的表单, 包括注册、登陆、基本资料修改、密码修改、邮箱修改等。 """ from flask.ext import wtf @@ -24,7 +24,7 @@ class SigninForm(RedirectForm): openid_provider = wtf.HiddenField('openid_provider') def validate(self): - if self.errors: return False + if not super(RedirectForm, self).validate(): return False user = User.get_by_email(self.email.data) if not user: @@ -56,7 +56,7 @@ def validate_email(form, field): # UserOpenID(provider=session['openid_provider'], openid=session['current_openid'])]) # self.user.set_password(self.password.data) -class ProfileForm(RedirectForm): +class EditProfileForm(RedirectForm): nickname = wtf.TextField('nickname', validators=[wtf.Required(message=u'请填写昵称')]) # FIXME: 验证 slug 是否已经被占用 slug = wtf.TextField('slug', validators=[ @@ -71,14 +71,14 @@ class ProfileForm(RedirectForm): intro = wtf.TextAreaField('introduction', validators=[ wtf.Length(min=0, max=3000, message=u'个人介绍最多为3000个字')]) -class EditPassForm(RedirectForm): - old_password= wtf.PasswordField(u'当前密码', validators=[wtf.Required(message=u'请提供当前密码')]) - password = wtf.PasswordField(u'新密码', validators=[ \ - wtf.Required(message=u'请填写新密码,不能少与5位字符'), \ - wtf.EqualTo('confirm', message=u'两次输入的密码不一致'), \ - wtf.Length(min=5, max=20, message=u'密码应为5到20位字符') - ]) - confirm = wtf.PasswordField(u'确认密码', validators=[wtf.Required(message=u'请再次输入新密码')]) +class EditPasswordForm(RedirectForm): + old_password = wtf.PasswordField(u'当前密码', validators=[wtf.Required(message=u'请提供当前密码')]) + password = wtf.PasswordField(u'新密码', validators=[ + wtf.Required(message=u'请填写新密码,不能少与5位字符'), + wtf.Length(min=5, max=20, message=u'密码应为5到20位字符')]) + confirm = wtf.PasswordField(u'确认密码', validators=[ + wtf.Required(message=u'请再次输入新密码'), + wtf.EqualTo('password', message=u'两次输入的密码不一致')]) def validate_old_password(form, field): if not current_user.user.check_password(field.data): diff --git a/website/scriptfan/templates/activities/create.html b/website/scriptfan/templates/activities/create.html index 560601b..dc7a5d5 100644 --- a/website/scriptfan/templates/activities/create.html +++ b/website/scriptfan/templates/activities/create.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "layout.html" %} {% block title %}创建活动{% endblock %} diff --git a/website/scriptfan/templates/activities/index.html b/website/scriptfan/templates/activities/index.html index e9ab56a..5ee5c96 100644 --- a/website/scriptfan/templates/activities/index.html +++ b/website/scriptfan/templates/activities/index.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "layout.html" %} {% block title %}社区活动{% endblock %} diff --git a/website/scriptfan/templates/index.html b/website/scriptfan/templates/index.html index 0a8247e..9bed878 100644 --- a/website/scriptfan/templates/index.html +++ b/website/scriptfan/templates/index.html @@ -1,3 +1,3 @@ -{% extends "base.html" %} +{% extends "layout.html" %} {% block title %}首页{% endblock %} diff --git a/website/scriptfan/templates/base.html b/website/scriptfan/templates/layout.html similarity index 89% rename from website/scriptfan/templates/base.html rename to website/scriptfan/templates/layout.html index 5fb3136..875d5db 100644 --- a/website/scriptfan/templates/base.html +++ b/website/scriptfan/templates/layout.html @@ -38,10 +38,10 @@
@@ -68,7 +68,7 @@ 你尚未设置个性域名,个性域名提供更友好的页面页面地址。 {# TODO: 添加个性域名提示不再显示功能 #} 不再提示 - 前往设置 + 前往设置
{% endif %} @@ -78,7 +78,7 @@ 你尚未设置密码,为了帐号的安全,请尽快设置密码。 {# TODO: 添加密码设置提示不再显示功能 #} 不再提示 - 前往设置 + 前往设置
{% endif %} {% endif %} diff --git a/website/scriptfan/templates/user/_sidebar.html b/website/scriptfan/templates/user/_sidebar.html deleted file mode 100644 index 67e2289..0000000 --- a/website/scriptfan/templates/user/_sidebar.html +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/website/scriptfan/templates/users/_sidebar.html b/website/scriptfan/templates/users/_sidebar.html new file mode 100644 index 0000000..e9578c8 --- /dev/null +++ b/website/scriptfan/templates/users/_sidebar.html @@ -0,0 +1,9 @@ + diff --git a/website/scriptfan/templates/user/general.html b/website/scriptfan/templates/users/general.html similarity index 97% rename from website/scriptfan/templates/user/general.html rename to website/scriptfan/templates/users/general.html index 467ebfe..8c83441 100644 --- a/website/scriptfan/templates/user/general.html +++ b/website/scriptfan/templates/users/general.html @@ -1,4 +1,4 @@ -{% extends "user/layout.html" %} +{% extends "users/layout.html" %} {% block title %}修改基本资料{% endblock %} @@ -11,7 +11,7 @@ {% endblock %} {% block content_body %} - + {{ form.hidden_tag() }}
diff --git a/website/scriptfan/templates/user/layout.html b/website/scriptfan/templates/users/layout.html similarity index 88% rename from website/scriptfan/templates/user/layout.html rename to website/scriptfan/templates/users/layout.html index c58e642..a67676d 100644 --- a/website/scriptfan/templates/user/layout.html +++ b/website/scriptfan/templates/users/layout.html @@ -10,6 +10,6 @@

{{ self.title() }}

{% block content_body %}{% endblock %}
- {% include 'user/_sidebar.html' %} + {% include 'users/_sidebar.html' %}
{% endblock %} diff --git a/website/scriptfan/templates/user/openid.html b/website/scriptfan/templates/users/openid.html similarity index 93% rename from website/scriptfan/templates/user/openid.html rename to website/scriptfan/templates/users/openid.html index 64412e0..9dc8d35 100644 --- a/website/scriptfan/templates/user/openid.html +++ b/website/scriptfan/templates/users/openid.html @@ -1,4 +1,4 @@ -{% extends "user/layout.html" %} +{% extends "users/layout.html" %} {% block title %} 管理服务绑定 @@ -29,7 +29,7 @@ {% endblock %} {% block content_body %} - + {{ form.hidden_tag() }} {% for errors in form.errors.values() %}
{% for error in errors %} {{ error }} {% endfor %}
diff --git a/website/scriptfan/templates/user/password.html b/website/scriptfan/templates/users/password.html similarity index 95% rename from website/scriptfan/templates/user/password.html rename to website/scriptfan/templates/users/password.html index 49800c9..301419d 100644 --- a/website/scriptfan/templates/user/password.html +++ b/website/scriptfan/templates/users/password.html @@ -1,4 +1,4 @@ -{% extends "user/layout.html" %} +{% extends "users/layout.html" %} {% block title %} {% if current_user.user.password %} @@ -17,7 +17,7 @@ {% endblock %} {% block content_body %} - + {{ form.hidden_tag() }} {% if current_user.user.password %}
diff --git a/website/scriptfan/templates/user/profile.html b/website/scriptfan/templates/users/profile.html similarity index 100% rename from website/scriptfan/templates/user/profile.html rename to website/scriptfan/templates/users/profile.html diff --git a/website/scriptfan/templates/user/signin.html b/website/scriptfan/templates/users/signin.html similarity index 100% rename from website/scriptfan/templates/user/signin.html rename to website/scriptfan/templates/users/signin.html diff --git a/website/scriptfan/templates/user/signup.html b/website/scriptfan/templates/users/signup.html similarity index 100% rename from website/scriptfan/templates/user/signup.html rename to website/scriptfan/templates/users/signup.html diff --git a/website/scriptfan/templates/user/slug.html b/website/scriptfan/templates/users/slug.html similarity index 96% rename from website/scriptfan/templates/user/slug.html rename to website/scriptfan/templates/users/slug.html index 23d0e8d..8aac5f8 100644 --- a/website/scriptfan/templates/user/slug.html +++ b/website/scriptfan/templates/users/slug.html @@ -1,4 +1,4 @@ -{% extends "user/layout.html" %} +{% extends "users/layout.html" %} {% block title %}设置个性域名{% endblock %} diff --git a/website/scriptfan/views/__init__.py b/website/scriptfan/views/__init__.py index 7872b0b..8acc2ad 100644 --- a/website/scriptfan/views/__init__.py +++ b/website/scriptfan/views/__init__.py @@ -1,6 +1,8 @@ -#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + scriptfan.views + ~~~~~~~~~~~~~~~~~~~~ + View controllers package +""" -from site import siteapp -from user import userapp -from post import postapp -from activity import activityapp +import events, post, home, users \ No newline at end of file diff --git a/website/scriptfan/views/events.py b/website/scriptfan/views/events.py index 293be0a..8a4d81d 100644 --- a/website/scriptfan/views/events.py +++ b/website/scriptfan/views/events.py @@ -6,13 +6,14 @@ Views controllers for events """ +from datetime import datetime + from flask import Blueprint, render_template, redirect, flash, url_for from scriptfan import db from scriptfan.forms import EventForm from scriptfan.models import Event from flask.ext.login import current_user -from datetime import datetime blueprint = Blueprint("events", __name__) @@ -20,15 +21,15 @@ @blueprint.route('/', methods=['GET']) def index(): - activities = Activity.query.all() - return render_template('activities/index.html', activities=activities) + activities = Event.query.all() + return render_template('events/index.html', activities=activities) @blueprint.route('/create', methods=['GET', 'POST']) def create(): - form = ActivityForm(cref_enabled=False) + form = EventForm(cref_enabled=False) if form.validate_on_submit(): - activity = Activity() + activity = Event() form.populate_obj(activity) # 装填用户和创建时间等信息 @@ -39,4 +40,4 @@ def create(): flash(u'活动%s发布成功.' % form.data.get('title'), 'success') return redirect(url_for('.index')) else: - return render_template('activities/create.html', form=form) + return render_template('events/create.html', form=form) diff --git a/website/scriptfan/views/home.py b/website/scriptfan/views/home.py new file mode 100644 index 0000000..ed2b180 --- /dev/null +++ b/website/scriptfan/views/home.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +""" + scriptfan.views.home + ~~~~~~~~~~~~~~~~~~~~~~~ + Home view controller +""" + +from flask import Blueprint, render_template + + +blueprint = Blueprint('home', __name__) + + +@blueprint.route('/') +def index(): + return render_template('index.html') diff --git a/website/scriptfan/views/site.py b/website/scriptfan/views/site.py deleted file mode 100644 index 53aa563..0000000 --- a/website/scriptfan/views/site.py +++ /dev/null @@ -1,8 +0,0 @@ -#-*-coding:utf-8-*- -from flask import Blueprint, render_template - -siteapp = Blueprint("site", __name__) - -@siteapp.route("/") -def index(): - return render_template("index.html") diff --git a/website/scriptfan/views/user.py b/website/scriptfan/views/users.py similarity index 78% rename from website/scriptfan/views/user.py rename to website/scriptfan/views/users.py index 648ca89..4eb9d93 100644 --- a/website/scriptfan/views/user.py +++ b/website/scriptfan/views/users.py @@ -6,11 +6,13 @@ from flask.ext import login from flask.ext.login import current_user from flask.ext.openid import COMMON_PROVIDERS -from scriptfan.extensions import db, oid, login_manager +from scriptfan import db, oid, login_manager from scriptfan.models import User, UserOpenID -from scriptfan.forms.user import SignupForm, SigninForm, EditProfileForm, EditPasswordForm, EditSlugForm, ManageOpenIDForm +from scriptfan.forms.user import SignupForm, SigninForm, EditProfileForm, \ + EditPasswordForm, EditSlugForm, \ + ManageOpenIDForm -userapp = Blueprint("user", __name__) +blurprint = Blueprint('users', __name__) class Anonymous(login.AnonymousUser): user = User(nickname=u'游客', email='') @@ -23,7 +25,7 @@ def __init__(self, user): self.user = user login_manager.anonymous_user = Anonymous -login_manager.login_view = 'user.signin' +login_manager.login_view = 'users.signin' login_manager.login_message = u'需要登陆后才能访问本页' @login_manager.user_loader @@ -35,11 +37,11 @@ def login_user(user, remember=False): """ 登陆用户并更新最近登陆时间 """ login.login_user(LoginUser(user), remember=remember) user.login_time = datetime.now() - app.logger.info('* Updated current user: %s, %s', user.id, user.email) + app.logger.info('* Updated current users: %s, %s', user.id, user.email) # 开发资料修改页面中的OpenID绑定功能 # FIXME: 第一次接触OpenID,实现得有点绕,是否还有漏洞没考虑到?一起讨论吧 -@userapp.route('/openid/manage', methods=['GET', 'POST']) +@blurprint.route('/openid/manage', methods=['GET', 'POST']) @login.login_required def openid_manage(): form = ManageOpenIDForm(csrf_enabled=False) @@ -47,14 +49,14 @@ def openid_manage(): method = form.method.data provider = form.provider.data if method == 'add': - return redirect(url_for('user.openid_add', provider=provider)) + return redirect(url_for('users.openid_add', provider=provider)) elif method == 'delete': return _delete_openid(provider) registed_providers = [openid.provider for openid in current_user.user.openids] providers = [(provider, provider in registed_providers) for provider in COMMON_PROVIDERS] - return render_template('user/openid.html', providers = providers, form=form) + return render_template('users/openid.html', providers = providers, form=form) # 解除绑定的实现 def _delete_openid(provider): @@ -70,14 +72,14 @@ def _delete_openid(provider): 'success') else: flash(u'解除OpenID绑定时发生了错误!', 'error') - return redirect(url_for('user.openid_manage')) + return redirect(url_for('users.openid_manage')) # 添加绑定的实现 -@userapp.route('/openid/add//', methods=['GET']) +@blurprint.route('/openid/add//', methods=['GET']) @login.login_required @oid.loginhandler def openid_add(provider): - next_url = url_for('user.openid_manage') + next_url = url_for('users.openid_manage') if provider not in COMMON_PROVIDERS: app.logger.warning('Invalid openid provider: %s' % provider) flash(u'暂不支持绑定到 %s ' % provider, 'warning') @@ -101,12 +103,12 @@ def openid_add(provider): return oid.try_login(COMMON_PROVIDERS.get(provider), \ ask_for=['email', 'fullname', 'nickname']) -@userapp.route('/openid//', methods=['GET']) +@blurprint.route('/openid//', methods=['GET']) @oid.loginhandler def openid(provider, next=None): if current_user.is_authenticated(): app.logger.info('User authenticated, redirecting to profile page') - return redirect(url_for('user.profile')) + return redirect(url_for('users.profile')) next_url = oid.get_next_url() if provider not in COMMON_PROVIDERS: @@ -128,21 +130,21 @@ def openid(provider, next=None): ask_for=['email', 'fullname', 'nickname']) -@userapp.route('/signin/', methods=['GET', 'POST']) +@blurprint.route('/signin/', methods=['GET', 'POST']) def signin(): # 如果用户已经登陆,跳转到用户资料页面 if current_user.is_authenticated(): - return redirect(url_for('user.profile')) + return redirect(url_for('users.profile')) form = SigninForm(csrf_enabled=False) if form.validate_on_submit(): - app.logger.info('Signin user: %s', form.email.data) + app.logger.info('Signin users: %s', form.email.data) login_user(form.user, remember=form.remember) flash(u'登陆成功', 'success') - return form.redirect('user.profile') + return form.redirect('users.profile') - return render_template('user/signin.html', form=form) + return render_template('users/signin.html', form=form) @oid.after_login @@ -161,7 +163,7 @@ def create_or_login(resp): # 这个方法用于绑定OpenID到已有帐号 def _regist_openid(user_id, resp): - redirect_url = url_for('user.openid_manage') + redirect_url = url_for('users.openid_manage') # 清理Session里的键,这样不影响以后的登陆 provider = session.get('openid_provider', None) session.pop('openid_provider', None) @@ -180,7 +182,7 @@ def _regist_openid(user_id, resp): # 绑定这个OpenID openid = UserOpenID(openid=resp.identity_url, provider=provider) current_user.user.openids.append(openid) - app.logger.info('bind openid: %s to user: %s', resp.identity_url, current_user.user) + app.logger.info('bind openid: %s to users: %s', resp.identity_url, current_user.user) flash(u'成功绑定到 %s 帐户,你可以用该帐户直接登陆了!' % provider, 'success') return redirect(redirect_url) else: @@ -199,28 +201,28 @@ def _create_or_login(resp): # 如果邮箱已经被注册,提示手工绑定 if User.query.filter_by(email=resp.email).first(): flash(u'邮箱 %s 已经被注册,如果你是该帐户的拥有者,请登陆后再绑定OpenID' % resp.email, 'warning') - return redirect(url_for('user.signin')) + return redirect(url_for('users.signin')) # 邮箱没有注册,自动创建帐户并登陆 - app.logger.info('Creating user with openid: %s', resp.identity_url) + app.logger.info('Creating users with openid: %s', resp.identity_url) user = User(email=resp.email, nickname=resp.nickname or resp.fullname) openid = UserOpenID(openid=resp.identity_url, provider=session['openid_provider']) user.openids.append(openid) db.session.add(user) db.session.commit() - flash(u'帐号已经创建, 可以在资料修改页面补充密码等信息'% url_for('user.general'), 'success') + flash(u'帐号已经创建, 可以在资料修改页面补充密码等信息'% url_for('users.general'), 'success') - app.logger.info('Signin user: %s', user.email) + app.logger.info('Signin users: %s', user.email) login_user(user, remember=True) flash(u'登陆成功') return redirect(oid.get_next_url()) -@userapp.route('/signup/', methods=['GET', 'POST']) +@blurprint.route('/signup/', methods=['GET', 'POST']) def signup(): if current_user.is_authenticated(): - return redirect(url_for('user.profile')) + return redirect(url_for('users.profile')) form = SignupForm(csrf_enabled=False) app.logger.info('Signup with email: %(email)s, nickname: %(nickname)s', form.data) @@ -230,14 +232,14 @@ def signup(): form.populate_obj(user) user.set_password(form.password1.data) db.session.add(user) - app.logger.info(u'New user added: %s', user) + app.logger.info(u'New users added: %s', user) flash(u'注册成功', 'success') - return redirect(url_for('user.signin')) + return redirect(url_for('users.signin')) - return render_template('user/signup.html', form=form) + return render_template('users/signup.html', form=form) -@userapp.route('/profile/') -@userapp.route('/profile/') +@blurprint.route('/profile/') +@blurprint.route('/profile/') @login.login_required def profile(slug_or_id=None): if slug_or_id: @@ -245,24 +247,24 @@ def profile(slug_or_id=None): user = User.query.get(int(slug_or_id)).first() else: user = User.query.filter_by(slug=slug_or_id).first() - return user and render_template('user/profile.html', user=user) or abort(404) + return user and render_template('users/profile.html', user=user) or abort(404) else: - return render_template('user/profile.html', user=current_user.user) + return render_template('users/profile.html', user=current_user.user) -@userapp.route('/general', methods=['GET', 'POST']) +@blurprint.route('/general', methods=['GET', 'POST']) @login.login_required def general(): form = EditProfileForm(csrf_enabled=False) if form.validate_on_submit(): - app.logger.info(u'* Updating user information...') + app.logger.info(u'* Updating users information...') app.logger.info(u'Form data: %s', repr(form.data)) form.populate_obj(current_user.user) flash(u'用户资料已经更新', 'success') - return form.redirect('user.general') + return form.redirect('users.general') # 如果是编辑用户信息,则使用用户当前信息填充表单 form.process(obj=current_user.user) - return render_template('user/general.html', form=form) + return render_template('users/general.html', form=form) # TODO 处理更新用户资料的请求 # TODO 用户照片上传 @@ -270,41 +272,41 @@ def general(): # TODO: 用户找回密码功能 # 更新用户slug功能 -@userapp.route('/slug', methods=['GET', 'POST']) +@blurprint.route('/slug', methods=['GET', 'POST']) @login.login_required def slug(): form = EditSlugForm() if form.validate_on_submit(): form.populate_obj(current_user.user) flash(u'修改域名已经设置', 'success') - return redirect(url_for('user.profile', slug=current_user.user.slug)) + return redirect(url_for('users.profile', slug=current_user.user.slug)) form.process(obj=current_user.user) - return render_template('user/slug.html', form=form, skip_slug_info=True) + return render_template('users/slug.html', form=form, skip_slug_info=True) -@userapp.route('/password', methods=['GET', 'POST']) +@blurprint.route('/password', methods=['GET', 'POST']) @login.login_required def password(): form = EditPasswordForm(csrf_enabled=False) if form.validate_on_submit(): current_user.user.set_password(form.password.data) flash(u'用户密码已经更新', 'success') - return form.redirect('user.general') + return form.redirect('users.general') form.errors and flash(u'用户密码未能更新', 'error') - return render_template('user/password.html', form=form, skip_password_info=True) + return render_template('users/password.html', form=form, skip_password_info=True) -@userapp.route('/email') +@blurprint.route('/email') @login.login_required def editemail(): return 'email' -@userapp.route('/signout/', methods=['GET']) +@blurprint.route('/signout/', methods=['GET']) @login.login_required def signout(): login.logout_user() if 'openid_provider' in session: del session['openid_provider'] - return redirect(url_for('site.index')) + return redirect(url_for('home.index')) From 7a54e6c3775f24ec65ae4584e29a3159f1474825 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Tue, 26 Feb 2013 23:36:12 +0800 Subject: [PATCH 041/119] =?UTF-8?q?=E9=87=8D=E5=91=BD=E5=90=8D=20activity?= =?UTF-8?q?=20=E7=9B=B8=E5=85=B3=E5=8F=98=E9=87=8F=E4=B8=BA=20event?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/models/__init__.py | 27 +------------------ website/scriptfan/models/event.py | 2 +- website/scriptfan/templates/events/_form.html | 2 +- .../scriptfan/templates/events/create.html | 6 ++--- website/scriptfan/templates/events/index.html | 18 ++++++------- website/scriptfan/views/events.py | 14 +++++----- 6 files changed, 22 insertions(+), 47 deletions(-) diff --git a/website/scriptfan/models/__init__.py b/website/scriptfan/models/__init__.py index 7bc3e88..069f0e9 100644 --- a/website/scriptfan/models/__init__.py +++ b/website/scriptfan/models/__init__.py @@ -37,29 +37,4 @@ # resources = db.relationship(Resource, secondary=topic_resources) # 话题相关资源 # # user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) -# users = db.relationship(User, backref='topics', lazy='dynamic') - - - - -# -# class ActivityComment(db.Model): -# """ -# 活动评论表 -# 如果是未注册用户使用openid注册,则仅将openid记录在cookie中 -# """ -# __tablename__ = 'activity_comments' -# -# id = db.Column(db.Integer, primary_key=True) -# author_name = db.Column(db.String(50), nullable=False) # 作者昵称 -# author_email = db.Column(db.String(255)) # 作者邮件地址 -# author_site = db.Column(db.String(255)) # 作者网址 -# content = db.Column(db.Text, nullable=False) # 评论内容 -# created_time = db.Column(db.DateTime) # 创建日期 -# modified_time = db.Column(db.DateTime) # 更新日期 -# -# parent_id = db.Column(db.Integer, db.ForeignKey('activity_comments.id'), nullable=True) -# children = db.relationship('ActivityComment', backref='parent', remote_side=[id]) # 回复评论的引用 -# -# user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) -# users = db.relationship('User', backref=db.backref('comments', lazy='dynamic')) +# users = db.relationship(User, backref='topics', lazy='dynamic') \ No newline at end of file diff --git a/website/scriptfan/models/event.py b/website/scriptfan/models/event.py index cb4e06b..20c907c 100644 --- a/website/scriptfan/models/event.py +++ b/website/scriptfan/models/event.py @@ -45,7 +45,7 @@ class Event(db.Model): # 活动相关资源 event_resources = db.Table('event_resources', - db.Column('activity_id', db.Integer, db.ForeignKey('events.id'), + db.Column('event_id', db.Integer, db.ForeignKey('events.id'), primary_key=True), db.Column('resource_id', db.Integer, db.ForeignKey('resources.id'), primary_key=True), diff --git a/website/scriptfan/templates/events/_form.html b/website/scriptfan/templates/events/_form.html index 57395d8..d61d991 100644 --- a/website/scriptfan/templates/events/_form.html +++ b/website/scriptfan/templates/events/_form.html @@ -1,5 +1,5 @@ {% macro form(form, action=None, class="form-horizontal") -%} - + {{ form.hidden_tag() }}
diff --git a/website/scriptfan/templates/events/create.html b/website/scriptfan/templates/events/create.html index dc7a5d5..943f869 100644 --- a/website/scriptfan/templates/events/create.html +++ b/website/scriptfan/templates/events/create.html @@ -3,12 +3,12 @@ {% block title %}创建活动{% endblock %} {% block styles %} -{{ t.css('css/activity.css') }} +{{ t.css('css/event.css') }} {% endblock %} {% block scripts %} {{ t.js('http://maps.google.com/maps/api/js?v=3.exp&sensor=false&libraries=places', external=true) }} -{{ t.js('js/activity.js') }} +{{ t.js('js/event.js') }} {% endblock %} {% block content %} @@ -18,7 +18,7 @@

创建活动

{% import 'activities/_form.html' as f %} - {{ f.form(form, action=url_for('activity.create')) }} + {{ f.form(form, action=url_for('event.create')) }}
{% endblock %} diff --git a/website/scriptfan/templates/events/index.html b/website/scriptfan/templates/events/index.html index 5ee5c96..993bcd5 100644 --- a/website/scriptfan/templates/events/index.html +++ b/website/scriptfan/templates/events/index.html @@ -5,19 +5,19 @@ {% block content %}
-
    - {% for activity in activities %} -
  • -
    -

    {{ activity.title }}

    - {{ activity.created_time | dateformat }} +
      + {% for event in events %} +
    • +
      +

      {{ event.title }}

      + {{ event.created_time | dateformat }}
      -
      - {{ activity.content | markdown | safe }} +
      + {{ event.content | markdown | safe }}
    • {% endfor %} diff --git a/website/scriptfan/views/events.py b/website/scriptfan/views/events.py index 8a4d81d..d1d96cc 100644 --- a/website/scriptfan/views/events.py +++ b/website/scriptfan/views/events.py @@ -21,22 +21,22 @@ @blueprint.route('/', methods=['GET']) def index(): - activities = Event.query.all() - return render_template('events/index.html', activities=activities) + events = Event.query.all() + return render_template('events/index.html', events=events) @blueprint.route('/create', methods=['GET', 'POST']) def create(): form = EventForm(cref_enabled=False) if form.validate_on_submit(): - activity = Event() - form.populate_obj(activity) + event = Event() + form.populate_obj(event) # 装填用户和创建时间等信息 - activity.user_id = current_user.user.id + event.user_id = current_user.user.id # TODO: 使用一些让SQLAlchemy能够自动更新模型中的 created_time 和 modified_time - activity.created_time = datetime.now() - db.session.add(activity) + event.created_time = datetime.now() + db.session.add(event) flash(u'活动%s发布成功.' % form.data.get('title'), 'success') return redirect(url_for('.index')) else: From 35d4c34b86d0e23238c84c3919a595375fc71761 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 27 Feb 2013 00:39:20 +0800 Subject: [PATCH 042/119] Create EventDuration Model (Sync with database) --- website/scriptfan/models/__init__.py | 1 + website/scriptfan/models/event.py | 20 ++++++------ website/scriptfan/models/event_duration.py | 37 ++++++++++++++++++++++ website/scriptfan/models/user.py | 2 +- 4 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 website/scriptfan/models/event_duration.py diff --git a/website/scriptfan/models/__init__.py b/website/scriptfan/models/__init__.py index 069f0e9..8aa36cb 100644 --- a/website/scriptfan/models/__init__.py +++ b/website/scriptfan/models/__init__.py @@ -9,6 +9,7 @@ from .user import User from .user_openid import UserOpenID from .event import Event +from .event_duration import EventDuration from .resource import Resource # 活动相关资源 diff --git a/website/scriptfan/models/event.py b/website/scriptfan/models/event.py index 20c907c..93bd60e 100644 --- a/website/scriptfan/models/event.py +++ b/website/scriptfan/models/event.py @@ -16,29 +16,29 @@ class Event(db.Model): __tablename__ = 'events' id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('users.id')) # 发起人 + creator_id = db.Column(db.Integer, db.ForeignKey('users.id')) # 发起人 title = db.Column(db.String(255)) # 活动标题 content = db.Column(db.Text) # 活动介绍 - slug = db.Column(db.String(255)) # 页面地址 - start_time = db.Column(db.DateTime) # 活动开始时间 - end_time = db.Column(db.DateTime) # 活动结束时间 + content_html = db.Column(db.Text) # 转换后的省劲介绍内容 address = db.Column(db.String(255)) # 活动地址 - longitude = db.Column(db.Numeric(10, 7)) # 经度 - latitude = db.Column(db.Numeric(10, 7)) # 纬度 + lat = db.Column(db.Numeric(10, 7)) # 纬度 + lng = db.Column(db.Numeric(10, 7)) # 经度 created_time = db.Column(db.DateTime) # 活动创建时间 - modified_time = db.Column(db.DateTime) # 活动更新时间 + updated_time = db.Column(db.DateTime) # 活动更新时间 - followers = db.relationship('User', secondary='event_users', + durations = db.relationship('EventDuration', backref=db.backref('event')) + + followers = db.relationship('User', secondary='event_members', backref=db.backref('events', lazy='dynamic')) # 参与者 resources = db.relationship('Resource', secondary='event_resources', backref=db.backref('events', lazy='dynamic')) # 话题相关资源 # 用户参与活动的跟踪表 -event_users = db.Table('event_users', +event_members = db.Table('event_members', db.Column('event_id', db.Integer, db.ForeignKey('events.id'), primary_key=True), - db.Column('user_id', db.Integer, db.ForeignKey('users.id'), + db.Column('member_id', db.Integer, db.ForeignKey('users.id'), primary_key=True), ) diff --git a/website/scriptfan/models/event_duration.py b/website/scriptfan/models/event_duration.py new file mode 100644 index 0000000..565cb68 --- /dev/null +++ b/website/scriptfan/models/event_duration.py @@ -0,0 +1,37 @@ +# -*-coding: utf-8-*- +""" + scriptfan.models.event_duration + ~~~~~~~~~~~~~~~~~~~~~~ + + Model for table: event_durations +""" + +from scriptfan import db + +class EventDuration(db.Model): + """Event duration assignment + + Samples: + * 1 Day + - Date: 2013-03-16, Start_Time: 13:30, End_Time: 17:00 + * 2 Days + - Date: 2013-03-16, Start_Time: 09:00, End_Time: 17:00 + - Date: 2013-03-17, Start_Time: 09:30, End_Time: 11:30 + + It seems that ScriptFan will not create an event that repeats by + week/month/year, so the data model is enough. + """ + + __tablename__ = 'event_durations' + + id = db.Column(db.Integer, primary_key=True) + + #: Date part o event. 2013-03-16 + date = db.Column(db.Date) + #: Start time of date part. 13:30 + start_time = db.Column(db.Time, default='13:30') + #: End time of date part. 17:00 + end_time = db.Column(db.Time, default='17:00') + + #: Related event of time duration + event_id = db.Column(db.Integer, db.ForeignKey('events.id'), nullable=False) \ No newline at end of file diff --git a/website/scriptfan/models/user.py b/website/scriptfan/models/user.py index 328ab17..2926d92 100644 --- a/website/scriptfan/models/user.py +++ b/website/scriptfan/models/user.py @@ -49,7 +49,7 @@ class User(db.Model): privilege = db.Column(db.Integer, default=3) #: 用户 openid 的绑定列表 - openids = db.relationship('UserOpenID', backref=db.backref('users')) + openids = db.relationship('UserOpenID', backref=db.backref('user')) def __repr__(self): return u'' % (self.nickname, self.email) From 3f0cde48f2bd436a255f973855789b4b48485826 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 27 Feb 2013 00:43:46 +0800 Subject: [PATCH 043/119] Fix errors for events url mapping --- website/scriptfan/templates/events/create.html | 4 ++-- website/scriptfan/templates/events/index.html | 2 +- website/scriptfan/views/events.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/website/scriptfan/templates/events/create.html b/website/scriptfan/templates/events/create.html index 943f869..4e279e5 100644 --- a/website/scriptfan/templates/events/create.html +++ b/website/scriptfan/templates/events/create.html @@ -17,8 +17,8 @@

      创建活动

    - {% import 'activities/_form.html' as f %} - {{ f.form(form, action=url_for('event.create')) }} + {% import 'events/_form.html' as f %} + {{ f.form(form, action=url_for('events.create')) }}
{% endblock %} diff --git a/website/scriptfan/templates/events/index.html b/website/scriptfan/templates/events/index.html index 993bcd5..47e844a 100644 --- a/website/scriptfan/templates/events/index.html +++ b/website/scriptfan/templates/events/index.html @@ -5,7 +5,7 @@ {% block content %}
diff --git a/website/scriptfan/views/events.py b/website/scriptfan/views/events.py index d1d96cc..1eb6bc0 100644 --- a/website/scriptfan/views/events.py +++ b/website/scriptfan/views/events.py @@ -38,6 +38,6 @@ def create(): event.created_time = datetime.now() db.session.add(event) flash(u'活动%s发布成功.' % form.data.get('title'), 'success') - return redirect(url_for('.index')) + return redirect(url_for('events.index')) else: return render_template('events/create.html', form=form) From a935a7d830e26800069035b7b5ed0b980968e37c Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 27 Feb 2013 00:56:23 +0800 Subject: [PATCH 044/119] Improve event creation form 1. Separate duration date, start_time and end_time 2. Set default values for duration --- website/scriptfan/forms/event.py | 8 +++++--- website/scriptfan/templates/events/_form.html | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/website/scriptfan/forms/event.py b/website/scriptfan/forms/event.py index b3af673..f4992e2 100644 --- a/website/scriptfan/forms/event.py +++ b/website/scriptfan/forms/event.py @@ -7,16 +7,18 @@ from flask.ext import wtf from scriptfan.forms.base import RedirectForm - +from datetime import date class EventForm(RedirectForm): title = wtf.TextField(u'活动标题', validators=[ \ wtf.Required(message=u'请为活动填写一个标题')]) content = wtf.TextAreaField(u'活动简介', validators=[ \ wtf.Length(min=10, max=5000, message=u'简介至少10个字')]) - start_time = wtf.TextField(u'开始时间', validators=[ \ + date = wtf.TextField(u'活动日期', default=date.today, + validators=[ wtf.Required(message=u'需要指定活动日期')]) + start_time = wtf.TextField(u'开始时间', default='13:30', validators=[ \ wtf.Required(message=u'需要指定开始时间')]) - end_time = wtf.TextField(u'结束时间', validators=[ \ + end_time = wtf.TextField(u'结束时间', default='17:00', validators=[ \ wtf.Required(message=u'需要指定结束时间')]) address = wtf.TextField(u'活动地点') latitude = wtf.HiddenField() diff --git a/website/scriptfan/templates/events/_form.html b/website/scriptfan/templates/events/_form.html index d61d991..42c333c 100644 --- a/website/scriptfan/templates/events/_form.html +++ b/website/scriptfan/templates/events/_form.html @@ -12,7 +12,9 @@
- {{ form.start_time }}  至  {{ form.end_time }} + {{ form.date(class='input-small', placeholder='请输入日期') }}   + {{ form.start_time(class='input-mini') }} - + {{ form.end_time(class='input-mini') }} {{ form.start_time | error_text }} {{ form.end_time | error_text }} From 92e3bd917961717951eac96beffbd2646b09963e Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 27 Feb 2013 02:21:30 +0800 Subject: [PATCH 045/119] Fix mysql chinese characters errors 1. Add notes in readme to create database with collation: utf8 2. Fix url error in user signin and signup form. 3. Update scriptfan and alebic conig file database url --- README.md | 2 ++ website/alembic.ini.sample | 2 +- website/scriptfan/scriptfan.cfg.sample | 2 +- website/scriptfan/templates/users/signin.html | 4 ++-- website/scriptfan/templates/users/signup.html | 4 ++-- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5ce312b..875411d 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ ScriptFan.com 是西安一个线下技术沙龙的官方网站程序, 沙龙的 在本地数据库中建立一个数据库,如 ``scriptfan_dev`` +**注意:** 请确保数据库、表及字段的 Collation 为 utf8 (utf8_bin) 类型 + 修改配置文件 $ cp website/scriptfan/scriptfan.cfg.sample website/scriptfan/scriptfan.cfg diff --git a/website/alembic.ini.sample b/website/alembic.ini.sample index 2e3080c..bcc3ef1 100644 --- a/website/alembic.ini.sample +++ b/website/alembic.ini.sample @@ -11,7 +11,7 @@ script_location = migrate # the 'revision' command, regardless of autogenerate # revision_environment = false -sqlalchemy.url = mysql://root:root@localhost/scriptfan_dev?charset=utf8 +sqlalchemy.url = mysql://root:root@localhost/scriptfan_dev?charset=utf8&use_unicode=1 # Logging configuration diff --git a/website/scriptfan/scriptfan.cfg.sample b/website/scriptfan/scriptfan.cfg.sample index 7e2a009..778adc3 100644 --- a/website/scriptfan/scriptfan.cfg.sample +++ b/website/scriptfan/scriptfan.cfg.sample @@ -12,7 +12,7 @@ SECRET_KEY='youshouldnotknowthis' PAGE_SIZE=20 # database settings -SQLALCHEMY_DATABASE_URI='sqlite:///db.sqlite' +SQLALCHEMY_DATABASE_URI='mysql://root:root@localhost/scriptfan_dev?charset=utf8&use_unicode=1' SQLALCHEMY_ECHO=False # LOGGING CONFIG diff --git a/website/scriptfan/templates/users/signin.html b/website/scriptfan/templates/users/signin.html index 156a6ca..d39e0dc 100644 --- a/website/scriptfan/templates/users/signin.html +++ b/website/scriptfan/templates/users/signin.html @@ -8,7 +8,7 @@

{{ self.title() }}

- + {{ form.hidden_tag() }}
@@ -29,7 +29,7 @@

{{ self.title() }}

-
使用 Gmail帐户 登陆
+
使用 Gmail帐户 登陆
diff --git a/website/scriptfan/templates/users/signup.html b/website/scriptfan/templates/users/signup.html index 5d19424..0b8b907 100644 --- a/website/scriptfan/templates/users/signup.html +++ b/website/scriptfan/templates/users/signup.html @@ -8,7 +8,7 @@

{{ self.title() }}

-
+ {{ form.hidden_tag() }}
@@ -40,7 +40,7 @@

{{ self.title() }}

-   或者直接 使用Google登陆 +   或者直接 使用Google登陆
From 75ba0f2c700d99b19033bb00325278f1935fb265 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 27 Feb 2013 02:23:52 +0800 Subject: [PATCH 046/119] Update event creation. --- website/scriptfan/views/events.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/scriptfan/views/events.py b/website/scriptfan/views/events.py index 1eb6bc0..eaccd63 100644 --- a/website/scriptfan/views/events.py +++ b/website/scriptfan/views/events.py @@ -11,7 +11,7 @@ from flask import Blueprint, render_template, redirect, flash, url_for from scriptfan import db from scriptfan.forms import EventForm -from scriptfan.models import Event +from scriptfan.models import Event, EventDuration from flask.ext.login import current_user @@ -29,8 +29,12 @@ def index(): def create(): form = EventForm(cref_enabled=False) if form.validate_on_submit(): + # TODO: Create event for more than one day. event = Event() + duration = EventDuration() form.populate_obj(event) + form.populate_obj(duration) + event.durations.append(duration) # 装填用户和创建时间等信息 event.user_id = current_user.user.id From be83c2d06c88c1d95567937b57ed8e4ba1fbcd2d Mon Sep 17 00:00:00 2001 From: David Xie Date: Thu, 28 Feb 2013 10:50:57 +0800 Subject: [PATCH 047/119] add a shell script to update transaltion --- website/scriptfan/bin/translate.sh | 3 ++ website/scriptfan/messages.pot | 32 ++++-------------- website/scriptfan/templates/layout.html | 14 ++++---- .../zh_CN/LC_MESSAGES/messages.mo | Bin 550 -> 166 bytes 4 files changed, 17 insertions(+), 32 deletions(-) create mode 100755 website/scriptfan/bin/translate.sh diff --git a/website/scriptfan/bin/translate.sh b/website/scriptfan/bin/translate.sh new file mode 100755 index 0000000..92f6f25 --- /dev/null +++ b/website/scriptfan/bin/translate.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +msgfmt messages.pot -o translations/zh_CN/LC_MESSAGES/messages.mo diff --git a/website/scriptfan/messages.pot b/website/scriptfan/messages.pot index 432e029..9bc0616 100644 --- a/website/scriptfan/messages.pot +++ b/website/scriptfan/messages.pot @@ -1,34 +1,16 @@ -# Translations template for PROJECT. -# Copyright (C) 2012 ORGANIZATION -# This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2012. -# -msgid "" -msgstr "" -"Project-Id-Version: PROJECT VERSION\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2012-12-04 22:17+0800\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" +# ScriptFan.com -#: templates/index.html:2 templates/layout.html:28 -msgid "Index" +msgid "index" msgstr "首页" -#: templates/layout.html:29 -msgid "Activities" +msgid "event" msgstr "活动" -#: templates/layout.html:48 -msgid "Signup" +msgid "signup" msgstr "注册" -#: templates/layout.html:49 -msgid "Sign In" +msgid "signin" msgstr "登陆" +msgid "logout" +msgstr "" diff --git a/website/scriptfan/templates/layout.html b/website/scriptfan/templates/layout.html index 4a6bf2a..edf6316 100644 --- a/website/scriptfan/templates/layout.html +++ b/website/scriptfan/templates/layout.html @@ -25,10 +25,10 @@ diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo index d2cda9e7fa84b2b3cc819a2323f1548082075065..00cb27b28c9d0996f2b31c58408e95f95a71bca1 100644 GIT binary patch literal 166 zcmca7#4?ou2v~qv28eBdm=%a)pmYJ0E(6kRKwJZ*o1t_Ul%5EsXF%!sKpF%Xff!^q zGY~TYacWs=UI{~HUP@{OLvdz$US=McDlK4mwq^Izt`!U~mrZ-Qa4W;}nY&-kY-4z~ KdBxMV9tHr$N+DSQ literal 550 zcmZvY&ubGw6vs#L2h2@R9t7WACTaFd%xp=Ec+5fc zsL-Pq>0LebfAQ8z{tI84Koxw_yoq9`tD@D`P!NU7;pyr!E}J4TmW0#L0H2*UO^Ohzsl#1 zzQB6IOAcYs3>lu_DTEHofP<(1Qk6pK4HzE^>!fej{(!Zveal%7q-BvFn~Bl|ht|IG z^rh6&;zDqB#~gOVzAPbVyl6b55F@ AQUCw| From 0e7b76e611a5dcf174f2606508771f7a5915338b Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 13 Mar 2013 00:15:14 +0800 Subject: [PATCH 048/119] Replace mysql_python with mysql-connector-python --- requirements.txt | 2 +- website/alembic.ini.sample | 2 +- website/scriptfan/scriptfan.cfg.sample | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 40dbe70..3b5b461 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ flask-admin >= 1.0.1 flask-openid >= 1.0.1 flask-principal >= 0.3 flask-babel >= 0.8 -mysql-python >= 1.2.4 +mysql-connector-python >= 1.0.9 sqlalchemy >= 0.7 alembic >= 0.4.0 markdown2 >= 2.1.0 diff --git a/website/alembic.ini.sample b/website/alembic.ini.sample index bcc3ef1..71e23ab 100644 --- a/website/alembic.ini.sample +++ b/website/alembic.ini.sample @@ -11,7 +11,7 @@ script_location = migrate # the 'revision' command, regardless of autogenerate # revision_environment = false -sqlalchemy.url = mysql://root:root@localhost/scriptfan_dev?charset=utf8&use_unicode=1 +sqlalchemy.url = mysql+mysqlconnector://root:root@localhost/scriptfan_dev?charset=utf8&use_unicode=1 # Logging configuration diff --git a/website/scriptfan/scriptfan.cfg.sample b/website/scriptfan/scriptfan.cfg.sample index 778adc3..1f8721b 100644 --- a/website/scriptfan/scriptfan.cfg.sample +++ b/website/scriptfan/scriptfan.cfg.sample @@ -12,7 +12,7 @@ SECRET_KEY='youshouldnotknowthis' PAGE_SIZE=20 # database settings -SQLALCHEMY_DATABASE_URI='mysql://root:root@localhost/scriptfan_dev?charset=utf8&use_unicode=1' +SQLALCHEMY_DATABASE_URI='mysql+mysqlconnector://root:root@localhost/scriptfan_dev?charset=utf8&use_unicode=1' SQLALCHEMY_ECHO=False # LOGGING CONFIG From 10a45b756764c1c1cdecc83148d8d71b8df3fea8 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 13 Mar 2013 00:53:00 +0800 Subject: [PATCH 049/119] add database migrations for articles and categories. #12 --- .../504407e54867_create_articles_tabl.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 website/migrate/versions/504407e54867_create_articles_tabl.py diff --git a/website/migrate/versions/504407e54867_create_articles_tabl.py b/website/migrate/versions/504407e54867_create_articles_tabl.py new file mode 100644 index 0000000..4592814 --- /dev/null +++ b/website/migrate/versions/504407e54867_create_articles_tabl.py @@ -0,0 +1,36 @@ +"""create articles table + +Revision ID: 504407e54867 +Revises: 24c171278ca6 +Create Date: 2013-03-13 00:31:25.500000 + +""" + +# revision identifiers, used by Alembic. +revision = '504407e54867' +down_revision = '24c171278ca6' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('categories', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('name', sa.Integer, nullable=False, unique=True), + sa.Column('slug', sa.Integer, nullable=False, unique=True)) + + op.create_table('articles', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('title', sa.String(255), nullable=False), + sa.Column('type', sa.Integer), # news/anno/posts/ + sa.Column('content', sa.Text, nullable=False), + sa.Column('content_html', sa.Text, nullable=True), + sa.Column('author_id', sa.Integer), + sa.Column('category_id', sa.Integer), + sa.Column('created_time', sa.DateTime), + sa.Column('updated_time', sa.DateTime)) + +def downgrade(): + op.drop_table('articles') + op.drop_table('categories') From 9b1f8ef440e4b5d65894fb2b31c22f05afe42fbc Mon Sep 17 00:00:00 2001 From: greatghoul Date: Thu, 14 Mar 2013 00:51:10 +0800 Subject: [PATCH 050/119] =?UTF-8?q?=E4=BF=AE=E6=AD=A3i18n=20UnicodeDecodeE?= =?UTF-8?q?rror=20=E7=9A=84=E9=97=AE=E9=A2=98=20#8=20be83c2d06c88c1d955679?= =?UTF-8?q?37b57ed8e4ba1fbcd2d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/bin/translate.bat | 2 + website/scriptfan/messages.pot | 5 ++- .../zh_CN/LC_MESSAGES/messages.mo | Bin 166 -> 278 bytes .../zh_CN/LC_MESSAGES/messages.po | 35 ------------------ 4 files changed, 6 insertions(+), 36 deletions(-) create mode 100644 website/scriptfan/bin/translate.bat delete mode 100644 website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po diff --git a/website/scriptfan/bin/translate.bat b/website/scriptfan/bin/translate.bat new file mode 100644 index 0000000..d9a4a84 --- /dev/null +++ b/website/scriptfan/bin/translate.bat @@ -0,0 +1,2 @@ +@echo off +msgfmt messages.pot -o translations\zh_CN\LC_MESSAGES\messages.mo diff --git a/website/scriptfan/messages.pot b/website/scriptfan/messages.pot index 9bc0616..287275a 100644 --- a/website/scriptfan/messages.pot +++ b/website/scriptfan/messages.pot @@ -1,4 +1,7 @@ # ScriptFan.com +msgid "" +msgstr "" +"Content-Type: text/plain; charset=utf-8\n" msgid "index" msgstr "首页" @@ -13,4 +16,4 @@ msgid "signin" msgstr "登陆" msgid "logout" -msgstr "" +msgstr "注销" diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo index 00cb27b28c9d0996f2b31c58408e95f95a71bca1..bb6d31a5df1e3a7237f2469a60375d99ce7b2a16 100644 GIT binary patch literal 278 zcmca7#4?ou2-tvF28extm>Y;|fEWZ;05K~NuY}U;fiy_%Rw%s(N*@N&8bEvs%Kr?d ze?e(RMu, 2012. -# -msgid "" -msgstr "" -"Project-Id-Version: PROJECT VERSION\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2012-12-04 22:17+0800\n" -"PO-Revision-Date: 2012-12-04 22:12+0800\n" -"Last-Translator: FULL NAME \n" -"Language-Team: zh_CN \n" -"Plural-Forms: nplurals=1; plural=0\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" - -#: templates/index.html:2 templates/layout.html:28 -msgid "Index" -msgstr "首页" - -#: templates/layout.html:29 -msgid "Activities" -msgstr "活动" - -#: templates/layout.html:48 -msgid "Signup" -msgstr "注册" - -#: templates/layout.html:49 -msgid "Sign In" -msgstr "登陆" - From ed7c5cc930334dd826105995546f9d96322b35a2 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Thu, 14 Mar 2013 01:33:13 +0800 Subject: [PATCH 051/119] #12 Add articles related models --- website/scriptfan/models/__init__.py | 4 +++- website/scriptfan/models/article.py | 26 ++++++++++++++++++++++++++ website/scriptfan/models/category.py | 19 +++++++++++++++++++ website/scriptfan/models/user.py | 5 ++++- 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 website/scriptfan/models/article.py create mode 100644 website/scriptfan/models/category.py diff --git a/website/scriptfan/models/__init__.py b/website/scriptfan/models/__init__.py index 8aa36cb..fa3cf52 100644 --- a/website/scriptfan/models/__init__.py +++ b/website/scriptfan/models/__init__.py @@ -11,6 +11,8 @@ from .event import Event from .event_duration import EventDuration from .resource import Resource +from .article import Article +from .category import Category # 活动相关资源 # topic_resources = db.Table('topic_resources', @@ -38,4 +40,4 @@ # resources = db.relationship(Resource, secondary=topic_resources) # 话题相关资源 # # user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) -# users = db.relationship(User, backref='topics', lazy='dynamic') \ No newline at end of file +# users = db.relationship(User, backref='topics', lazy='dynamic') diff --git a/website/scriptfan/models/article.py b/website/scriptfan/models/article.py new file mode 100644 index 0000000..a31f14b --- /dev/null +++ b/website/scriptfan/models/article.py @@ -0,0 +1,26 @@ +# -*-coding: utf-8-*- +""" + scriptfan.models.articles + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Model for table: articles +""" + +from scriptfan import db +from datetime import datetime + +class Article(db.Model): + __tablename__ = 'articles' + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(255), nullable=False) + #: Article type news/posts/annoncements + type = db.Column(db.Integer) + content = db.Column(db.Text, nullable=True) + #: Parsed html content + content_html = db.Column(db.Text) + author_id = db.Column(db.Integer, db.ForeignKey('users.id')) + category_id = db.Column(db.Integer, db.ForeignKey('categories.id')) + created_time = db.Column(db.DateTime, default=datetime.now) + updated_time = db.Column(db.DateTime, default=datetime.now) + diff --git a/website/scriptfan/models/category.py b/website/scriptfan/models/category.py new file mode 100644 index 0000000..ad7f13e --- /dev/null +++ b/website/scriptfan/models/category.py @@ -0,0 +1,19 @@ +# -*-coding: utf-8-*- +""" + scriptfan.models.category + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Model for table: categories +""" + +from scriptfan import db +from datetime import datetime + +class Category(db.Model): + __tablename__ = 'categories' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255), nullable=False, unique=True) + slug = db.Column(db.String(255), nullable=False, unique=True) + + articles = db.relationship('Article', backref=db.backref('category')) diff --git a/website/scriptfan/models/user.py b/website/scriptfan/models/user.py index 2926d92..5b6a134 100644 --- a/website/scriptfan/models/user.py +++ b/website/scriptfan/models/user.py @@ -50,7 +50,10 @@ class User(db.Model): #: 用户 openid 的绑定列表 openids = db.relationship('UserOpenID', backref=db.backref('user')) - + + #: User articles + articles = db.relationship('Article', backref=db.backref('author')) + def __repr__(self): return u'' % (self.nickname, self.email) From c46e71e2f60c7dfee8c01187e2902c9d64157fa2 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Thu, 14 Mar 2013 08:11:24 +0800 Subject: [PATCH 052/119] #12 Add forms for article and category operations --- website/scriptfan/forms/__init__.py | 2 +- website/scriptfan/forms/articles.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 website/scriptfan/forms/articles.py diff --git a/website/scriptfan/forms/__init__.py b/website/scriptfan/forms/__init__.py index 9489ebe..77513ea 100644 --- a/website/scriptfan/forms/__init__.py +++ b/website/scriptfan/forms/__init__.py @@ -5,4 +5,4 @@ Forms package for scriptfan """ -from .event import EventForm \ No newline at end of file +from .event import EventForm diff --git a/website/scriptfan/forms/articles.py b/website/scriptfan/forms/articles.py new file mode 100644 index 0000000..fd41f2f --- /dev/null +++ b/website/scriptfan/forms/articles.py @@ -0,0 +1,28 @@ +#-*- coding: utf-8 -*- + +""" + scriptfan/forms/articles + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Forms for articles and categories +""" + +from flask.ext import wtf +from scriptfan.forms.base import RedirectForm + +class ArticleForm(RedirectForm): + """ Form for article create and update """ + + title = wtf.TextField('email', validators=[ + wtf.Required(message=u'请填写标题')] + type = wtf.PasswordField('type', validators=[ + wtf.Required(message=u'请选择类型')]) + content = wtf.TextArea('content', validators=[wtf.Required(message=u'文章内容不能为空'])) + # TODO: Fill article category dropdown with categories + category_id = wtf.SelectField('category_id') + +class CategoryForm(RedirectForm): + """ Form for category create and update """ + + name = wtf.TextField("name", validators=[wtf.Required(message=u'名称不能为空')]) + name = wtf.TextField("slug", validators=[wtf.Required(message=u'名称不能为空')]) From 5930cf51baf9986ced509b79308987ca704c14d4 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Thu, 14 Mar 2013 08:18:17 +0800 Subject: [PATCH 053/119] #12 Remove type field from table: articles --- .../308b76f8dee7_remove_type_field_fr.py | 22 +++++++++++++++++++ website/scriptfan/forms/articles.py | 2 -- website/scriptfan/models/article.py | 2 -- 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 website/migrate/versions/308b76f8dee7_remove_type_field_fr.py diff --git a/website/migrate/versions/308b76f8dee7_remove_type_field_fr.py b/website/migrate/versions/308b76f8dee7_remove_type_field_fr.py new file mode 100644 index 0000000..57229ca --- /dev/null +++ b/website/migrate/versions/308b76f8dee7_remove_type_field_fr.py @@ -0,0 +1,22 @@ +"""remove type field from articles + +Revision ID: 308b76f8dee7 +Revises: 504407e54867 +Create Date: 2013-03-14 08:12:18.234000 + +""" + +# revision identifiers, used by Alembic. +revision = '308b76f8dee7' +down_revision = '504407e54867' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.drop_column('articles', 'type') + + +def downgrade(): + op.add_column('articles', sa.Column('type', sa.Integer)) diff --git a/website/scriptfan/forms/articles.py b/website/scriptfan/forms/articles.py index fd41f2f..a09b2a6 100644 --- a/website/scriptfan/forms/articles.py +++ b/website/scriptfan/forms/articles.py @@ -15,8 +15,6 @@ class ArticleForm(RedirectForm): title = wtf.TextField('email', validators=[ wtf.Required(message=u'请填写标题')] - type = wtf.PasswordField('type', validators=[ - wtf.Required(message=u'请选择类型')]) content = wtf.TextArea('content', validators=[wtf.Required(message=u'文章内容不能为空'])) # TODO: Fill article category dropdown with categories category_id = wtf.SelectField('category_id') diff --git a/website/scriptfan/models/article.py b/website/scriptfan/models/article.py index a31f14b..76dcbd9 100644 --- a/website/scriptfan/models/article.py +++ b/website/scriptfan/models/article.py @@ -14,8 +14,6 @@ class Article(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(255), nullable=False) - #: Article type news/posts/annoncements - type = db.Column(db.Integer) content = db.Column(db.Text, nullable=True) #: Parsed html content content_html = db.Column(db.Text) From 97551e50c59e1dbc46c6bb61bcc62e23e81f9e52 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Thu, 14 Mar 2013 22:50:50 +0800 Subject: [PATCH 054/119] Fix ArticleFomr syntax error --- website/scriptfan/__init__.py | 3 ++- website/scriptfan/forms/articles.py | 8 ++++---- website/scriptfan/views/articles.py | 21 +++++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 website/scriptfan/views/articles.py diff --git a/website/scriptfan/__init__.py b/website/scriptfan/__init__.py index 21688a4..ade01fe 100644 --- a/website/scriptfan/__init__.py +++ b/website/scriptfan/__init__.py @@ -73,10 +73,11 @@ def page_error(error): def register_blueprints(app): app.logger.info('Register blueprints...') - from scriptfan.views import home, events, users + from scriptfan.views import home, events, users, articles app.register_blueprint(home.blueprint, url_prefix='/') app.register_blueprint(users.blurprint, url_prefix='/users') app.register_blueprint(events.blueprint, url_prefix='/events') + app.register_blueprint(articles.blueprint, url_prefix='/articles') def register_jinja_env(app): diff --git a/website/scriptfan/forms/articles.py b/website/scriptfan/forms/articles.py index a09b2a6..eb1b677 100644 --- a/website/scriptfan/forms/articles.py +++ b/website/scriptfan/forms/articles.py @@ -13,14 +13,14 @@ class ArticleForm(RedirectForm): """ Form for article create and update """ - title = wtf.TextField('email', validators=[ - wtf.Required(message=u'请填写标题')] - content = wtf.TextArea('content', validators=[wtf.Required(message=u'文章内容不能为空'])) + title = wtf.TextField('email', validators=[wtf.Required(message=u'请填写标题')]) + content = wtf.TextAreaField('content', validators=[wtf.Required(message=u'文章内容不能为空')]) # TODO: Fill article category dropdown with categories category_id = wtf.SelectField('category_id') class CategoryForm(RedirectForm): """ Form for category create and update """ + # TODO: Add unique validations for name and slug name = wtf.TextField("name", validators=[wtf.Required(message=u'名称不能为空')]) - name = wtf.TextField("slug", validators=[wtf.Required(message=u'名称不能为空')]) + slug = wtf.TextField("slug", validators=[wtf.Required(message=u'名称不能为空')]) diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py new file mode 100644 index 0000000..5287043 --- /dev/null +++ b/website/scriptfan/views/articles.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +""" + scriptfan.views.articles + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Views controllers for articles and categoirs +""" + +from datetime import datetime + +from flask import Blueprint, render_template, redirect, flash, url_for +from scriptfan import db +from scriptfan.forms.articles import ArticleForm +from scriptfan.models import Article, Category + +from flask.ext.login import current_user + + +blueprint = Blueprint("articles", __name__) + + From 2cc0119494c61cce4124759af10f0601ae7729a7 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Thu, 14 Mar 2013 22:59:13 +0800 Subject: [PATCH 055/119] #12 Fix articles and categories form syntax error --- website/scriptfan/forms/articles.py | 1 - website/scriptfan/views/articles.py | 1 - 2 files changed, 2 deletions(-) diff --git a/website/scriptfan/forms/articles.py b/website/scriptfan/forms/articles.py index eb1b677..90d1d2b 100644 --- a/website/scriptfan/forms/articles.py +++ b/website/scriptfan/forms/articles.py @@ -21,6 +21,5 @@ class ArticleForm(RedirectForm): class CategoryForm(RedirectForm): """ Form for category create and update """ - # TODO: Add unique validations for name and slug name = wtf.TextField("name", validators=[wtf.Required(message=u'名称不能为空')]) slug = wtf.TextField("slug", validators=[wtf.Required(message=u'名称不能为空')]) diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 5287043..9a9bd5b 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -18,4 +18,3 @@ blueprint = Blueprint("articles", __name__) - From 1488c055f38a8bd09ec1f3f575d6e53df85d137c Mon Sep 17 00:00:00 2001 From: greatghoul Date: Thu, 14 Mar 2013 23:03:10 +0800 Subject: [PATCH 056/119] Fix slug edit page error and redirect error --- website/scriptfan/templates/users/slug.html | 2 +- website/scriptfan/views/users.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/website/scriptfan/templates/users/slug.html b/website/scriptfan/templates/users/slug.html index 8aac5f8..3683c0a 100644 --- a/website/scriptfan/templates/users/slug.html +++ b/website/scriptfan/templates/users/slug.html @@ -11,7 +11,7 @@ {% endblock %} {% block content_body %} - + {{ form.hidden_tag() }}
注意:个性域名设定后不能修改
diff --git a/website/scriptfan/views/users.py b/website/scriptfan/views/users.py index 4eb9d93..3c87156 100644 --- a/website/scriptfan/views/users.py +++ b/website/scriptfan/views/users.py @@ -238,6 +238,7 @@ def signup(): return render_template('users/signup.html', form=form) +# FIXME: Slug should not be same as an exists user_id @blurprint.route('/profile/') @blurprint.route('/profile/') @login.login_required @@ -279,7 +280,7 @@ def slug(): if form.validate_on_submit(): form.populate_obj(current_user.user) flash(u'修改域名已经设置', 'success') - return redirect(url_for('users.profile', slug=current_user.user.slug)) + return redirect(url_for('users.profile', slug_or_id=current_user.user.slug)) form.process(obj=current_user.user) return render_template('users/slug.html', form=form, skip_slug_info=True) From ee3b7a4cd210497b7ed9757680bfa2930a2a56f4 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Thu, 14 Mar 2013 23:10:31 +0800 Subject: [PATCH 057/119] #8 Add missed translations --- website/scriptfan/bin/translate.bat | 3 +-- website/scriptfan/messages.pot | 7 +++++++ .../translations/zh_CN/LC_MESSAGES/messages.mo | Bin 278 -> 348 bytes 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/website/scriptfan/bin/translate.bat b/website/scriptfan/bin/translate.bat index d9a4a84..faaeb8c 100644 --- a/website/scriptfan/bin/translate.bat +++ b/website/scriptfan/bin/translate.bat @@ -1,2 +1 @@ -@echo off -msgfmt messages.pot -o translations\zh_CN\LC_MESSAGES\messages.mo +msgfmt messages.pot -o translations\zh_CN\LC_MESSAGES\messages.mo -v diff --git a/website/scriptfan/messages.pot b/website/scriptfan/messages.pot index 287275a..685ffb7 100644 --- a/website/scriptfan/messages.pot +++ b/website/scriptfan/messages.pot @@ -1,4 +1,5 @@ # ScriptFan.com +# vim: ft=config msgid "" msgstr "" "Content-Type: text/plain; charset=utf-8\n" @@ -9,9 +10,15 @@ msgstr "首页" msgid "event" msgstr "活动" +msgid "my_profile" +msgstr "个人页面" + msgid "signup" msgstr "注册" +msgid "manage" +msgstr "设置" + msgid "signin" msgstr "登陆" diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo index bb6d31a5df1e3a7237f2469a60375d99ce7b2a16..b1c2f2877b302a7d9e01f9a88fac6cd12ed3234e 100644 GIT binary patch literal 348 zcmca7#4?ou2snUP28d&Tm>Y=a05J%h0Af}kJ_)7I18FuOz6z!90BJ5DehlTmgVJAt zv<49Wf$~)uA^LQnv@w*n0@6G{HI7ieACwLS(m=$(0>mJ+dwLww)HRo094CR<^TWy delta 184 zcmcb^G>xhLo)F7a1|VPqVi_Rz0b*_-t^r~YSOLVWK)e!4uLsf~wOgU|9w>bnNNWJ` zDJcIll>P;!85tq^IG{8?kOm?-1{SCSW*`j%APbp5oQZqoC%*IHdp>jb%b9Hq&o-}k I+SbDW0MTC;&;S4c From 7e4662f14c77ba770ee1aeab7dc6b2148dd13f73 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Thu, 14 Mar 2013 23:25:38 +0800 Subject: [PATCH 058/119] #12 Create article controller and article home page --- website/scriptfan/messages.pot | 6 ++++ .../scriptfan/templates/articles/index.html | 26 ++++++++++++++++++ website/scriptfan/templates/layout.html | 17 ++++++------ .../zh_CN/LC_MESSAGES/messages.mo | Bin 348 -> 433 bytes website/scriptfan/views/articles.py | 8 ++++++ 5 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 website/scriptfan/templates/articles/index.html diff --git a/website/scriptfan/messages.pot b/website/scriptfan/messages.pot index 685ffb7..03842a5 100644 --- a/website/scriptfan/messages.pot +++ b/website/scriptfan/messages.pot @@ -7,6 +7,12 @@ msgstr "" msgid "index" msgstr "首页" +msgid "articles" +msgstr "博客" + +msgid "articles.create" +msgstr "发布文章" + msgid "event" msgstr "活动" diff --git a/website/scriptfan/templates/articles/index.html b/website/scriptfan/templates/articles/index.html new file mode 100644 index 0000000..bede814 --- /dev/null +++ b/website/scriptfan/templates/articles/index.html @@ -0,0 +1,26 @@ +{% extends "layout.html" %} + +{% block title %}{{ _('articles') }}{% endblock %} + +{% block content %} +
+ + +
    + {% for article in articles %} +
  • +
    +

    {{ article.title }}

    + +
    +
    + {{ article.content | markdown | safe }} +
    +
  • + {% endfor %} +
+
+{% endblock %} diff --git a/website/scriptfan/templates/layout.html b/website/scriptfan/templates/layout.html index edf6316..b7d89de 100644 --- a/website/scriptfan/templates/layout.html +++ b/website/scriptfan/templates/layout.html @@ -11,14 +11,6 @@ {% block styles %}{% endblock %} - - - {{ t.js('lib/jquery-1.8.3.min.js') }} - {{ t.js('lib/bootstrap/js/bootstrap.min.js') }} - {{ t.js('js/application.js') }} - - - {% block scripts %}{% endblock %} + + + {{ t.js('lib/jquery-1.8.3.min.js') }} + {{ t.js('lib/bootstrap/js/bootstrap.min.js') }} + {{ t.js('js/application.js') }} + + + {% block scripts %}{% endblock %} diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo index b1c2f2877b302a7d9e01f9a88fac6cd12ed3234e..0408411985cde89484f0d0f1973e7c4e4d11b5b6 100644 GIT binary patch literal 433 zcmXw!ze~eF7=}N!{wj*%;H1#O!KHQ+Q9AiAxJ!uF8b~f7xk&4z2vtNW1r1aXw1Sih zSyU*k`lmQGjoEZ{^1XWfxQFN77+&vA;JBKxfiF{)U zOoCA$|1c=c6^3pykA{;q-DR3!#10r2Wbi#!BGWF~z938ET9J`e-gR8NV3>?NqsR?D zknhm8%>_1Pcghai%m`KzImgrtzLnAUHP>Sz?~6iqBTdoeO%ygLs@9_RNk2G^pYEvt u(v8lWlspE>{VVmKo5|G?eT5(Kdq}-@tJmpZJ-KaAT<<3JitLCg7xV{h7-ozB delta 214 zcmdnUe21z2o)F7a1|Z-7Vi_Qg0b*_-o&&@nZ~}-~f%qhpJ`beXfcPquz5}GWfcP<# z{|-uj1=1Qo{0GWcWrXO{fzrkh+LOTw$lw7ga)e6wLFrH+4MYqqKn!vmGZ2FS$Z~cd TW&&c6!(d=CCu8j7Dn@ev63-EC diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 9a9bd5b..25f350b 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -18,3 +18,11 @@ blueprint = Blueprint("articles", __name__) +@blueprint.route('/', methods=['GET']) +def index(): + articles = Article.query.all() + return render_template('articles/index.html') + +@blueprint.route('/create', methods=['GET', 'POST']) +def create(): + return 'Not implemented' From 5a06e46e6c59169a7a04c7f843ec1c374a5638ed Mon Sep 17 00:00:00 2001 From: iMom0 Date: Sat, 16 Mar 2013 21:58:01 +0800 Subject: [PATCH 059/119] Dump requirements.txt --- requirements.txt | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3b5b461..30b7d5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,25 @@ -flask >= 0.9 -flask-script >= 0.3.3 -flask-sqlalchemy >= 0.16 -flask-login >= 0.1.3 -flask-wtf >= 0.8 -flask-admin >= 1.0.1 -flask-openid >= 1.0.1 -flask-principal >= 0.3 -flask-babel >= 0.8 -mysql-connector-python >= 1.0.9 -sqlalchemy >= 0.7 -alembic >= 0.4.0 -markdown2 >= 2.1.0 +Babel==0.9.6 +Flask==0.9 +Flask-Admin==1.0.4 +Flask-Babel==0.8 +Flask-Login==0.1.3 +Flask-OpenID==1.1.1 +Flask-Principal==0.3.4 +Flask-SQLAlchemy==0.16 +Flask-Script==0.5.3 +Flask-WTF==0.8.3 +Jinja2==2.6 +Mako==0.7.3 +MarkupSafe==0.15 +MySQL-python==1.2.4 +SQLAlchemy==0.8.0 +WTForms==1.0.3 +Werkzeug==0.8.3 +alembic==0.4.2 +blinker==1.2 +markdown2==2.1.0 +mysql-connector-python==1.0.9 +python-openid==2.2.5 +pytz==2013b +speaklater==1.3 +wsgiref==0.1.2 From f78416d6e9ec9815c69016cf22241ee0bd563d0c Mon Sep 17 00:00:00 2001 From: iMom0 Date: Sat, 16 Mar 2013 22:32:36 +0800 Subject: [PATCH 060/119] Add the most simple article create html --- .../scriptfan/templates/articles/create.html | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 website/scriptfan/templates/articles/create.html diff --git a/website/scriptfan/templates/articles/create.html b/website/scriptfan/templates/articles/create.html new file mode 100644 index 0000000..fbc6aaa --- /dev/null +++ b/website/scriptfan/templates/articles/create.html @@ -0,0 +1,33 @@ +{% extends "layout.html" %} + +{% block title %}发表文章{% endblock %} + +{% block content %} +
+ + + + {{ form.hidden_tag() }} +
+ +
+ {{ form.title }} + {{ form.title | error_text }} +
+
+
+ +
+ {{ form.content }} + {{ form.content | error_text }} +
+
+
+ +
+ +
+{% endblock %} + From 1a3b760969f5807c3f2ef42084da6b36ff07cf1b Mon Sep 17 00:00:00 2001 From: iMom0 Date: Sat, 16 Mar 2013 22:54:12 +0800 Subject: [PATCH 061/119] Add first working create article view --- website/scriptfan/forms/articles.py | 8 ++++--- .../scriptfan/templates/articles/create.html | 2 +- website/scriptfan/views/articles.py | 22 ++++++++++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/website/scriptfan/forms/articles.py b/website/scriptfan/forms/articles.py index 90d1d2b..a7fb4a6 100644 --- a/website/scriptfan/forms/articles.py +++ b/website/scriptfan/forms/articles.py @@ -3,20 +3,22 @@ """ scriptfan/forms/articles ~~~~~~~~~~~~~~~~~~~~~~~~ - + Forms for articles and categories """ from flask.ext import wtf from scriptfan.forms.base import RedirectForm + class ArticleForm(RedirectForm): """ Form for article create and update """ - title = wtf.TextField('email', validators=[wtf.Required(message=u'请填写标题')]) + title = wtf.TextField('title', validators=[wtf.Required(message=u'请填写标题')]) content = wtf.TextAreaField('content', validators=[wtf.Required(message=u'文章内容不能为空')]) # TODO: Fill article category dropdown with categories - category_id = wtf.SelectField('category_id') + # category_id = wtf.SelectField('category_id') + class CategoryForm(RedirectForm): """ Form for category create and update """ diff --git a/website/scriptfan/templates/articles/create.html b/website/scriptfan/templates/articles/create.html index fbc6aaa..40d5fb2 100644 --- a/website/scriptfan/templates/articles/create.html +++ b/website/scriptfan/templates/articles/create.html @@ -8,7 +8,7 @@

{{ self.title() }}

-
+ {{ form.hidden_tag() }}
diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 25f350b..6ed94d0 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -6,23 +6,33 @@ Views controllers for articles and categoirs """ -from datetime import datetime +from flask import (Blueprint, render_template, redirect, + flash, url_for, request) +from flask.ext.login import current_user -from flask import Blueprint, render_template, redirect, flash, url_for from scriptfan import db from scriptfan.forms.articles import ArticleForm from scriptfan.models import Article, Category -from flask.ext.login import current_user - blueprint = Blueprint("articles", __name__) + @blueprint.route('/', methods=['GET']) def index(): articles = Article.query.all() - return render_template('articles/index.html') + return render_template('articles/index.html', + articles=articles) + @blueprint.route('/create', methods=['GET', 'POST']) def create(): - return 'Not implemented' + form = ArticleForm(request.form) + if request.method == 'POST' and form.validate(): + article = Article(title=form.title.data, + content=form.content.data) + db.session.add(article) + db.session.commit() + flash('Add article successfully!') + return redirect(url_for('.create')) + return render_template('articles/create.html', form=form) From 45ee9010742d525d6368f96b22cc779e2dffa138 Mon Sep 17 00:00:00 2001 From: iMom0 Date: Mon, 18 Mar 2013 12:28:15 +0800 Subject: [PATCH 062/119] Add update view --- website/scriptfan/models/article.py | 3 +++ .../articles/{create.html => form.html} | 2 +- website/scriptfan/views/articles.py | 22 ++++++++++++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) rename website/scriptfan/templates/articles/{create.html => form.html} (96%) diff --git a/website/scriptfan/models/article.py b/website/scriptfan/models/article.py index 76dcbd9..f93631d 100644 --- a/website/scriptfan/models/article.py +++ b/website/scriptfan/models/article.py @@ -22,3 +22,6 @@ class Article(db.Model): created_time = db.Column(db.DateTime, default=datetime.now) updated_time = db.Column(db.DateTime, default=datetime.now) + @classmethod + def get_by_id(cls, id): + return cls.query.filter_by(id=id).first() diff --git a/website/scriptfan/templates/articles/create.html b/website/scriptfan/templates/articles/form.html similarity index 96% rename from website/scriptfan/templates/articles/create.html rename to website/scriptfan/templates/articles/form.html index 40d5fb2..4ad85d2 100644 --- a/website/scriptfan/templates/articles/create.html +++ b/website/scriptfan/templates/articles/form.html @@ -1,6 +1,6 @@ {% extends "layout.html" %} -{% block title %}发表文章{% endblock %} +{% block title %}{{ title }}{% endblock %} {% block content %}
diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 6ed94d0..73a2179 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -27,12 +27,28 @@ def index(): @blueprint.route('/create', methods=['GET', 'POST']) def create(): - form = ArticleForm(request.form) - if request.method == 'POST' and form.validate(): + title = u'发表文章' + form = ArticleForm() + if form.validate_on_submit(): article = Article(title=form.title.data, content=form.content.data) db.session.add(article) db.session.commit() flash('Add article successfully!') return redirect(url_for('.create')) - return render_template('articles/create.html', form=form) + return render_template('articles/form.html', + form=form, title=title) + + +@blueprint.route('/edit/', methods=['GET', 'POST']) +def update(article_id): + title = u'修改文章' + article = Article.get_by_id(article_id) + form = ArticleForm(obj=article) + if form.validate_on_submit(): + form.populate_obj(article) + article.put() + flash('Update article successfully!') + return redirect(url_for('.index')) + return render_template('articles/form.html', + form=form, title=title) From f2637285952e60ae9d59c5377e42c34342080094 Mon Sep 17 00:00:00 2001 From: iMom0 Date: Mon, 18 Mar 2013 13:49:45 +0800 Subject: [PATCH 063/119] Remove action url in template --- website/scriptfan/templates/articles/form.html | 2 +- website/scriptfan/views/articles.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/scriptfan/templates/articles/form.html b/website/scriptfan/templates/articles/form.html index 4ad85d2..1b966e4 100644 --- a/website/scriptfan/templates/articles/form.html +++ b/website/scriptfan/templates/articles/form.html @@ -8,7 +8,7 @@

{{ self.title() }}

- + {{ form.hidden_tag() }}
diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 73a2179..06cfff4 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -47,7 +47,7 @@ def update(article_id): form = ArticleForm(obj=article) if form.validate_on_submit(): form.populate_obj(article) - article.put() + db.session.commit() flash('Update article successfully!') return redirect(url_for('.index')) return render_template('articles/form.html', From b99e8d38c03d28201cccb43e3a8b6f1648681016 Mon Sep 17 00:00:00 2001 From: iMom0 Date: Mon, 18 Mar 2013 23:27:45 +0800 Subject: [PATCH 064/119] Add translate for articles --- website/scriptfan/messages.pot | 9 +++++++++ website/scriptfan/templates/articles/form.html | 6 +++--- website/scriptfan/templates/index.html | 2 +- .../translations/zh_CN/LC_MESSAGES/messages.mo | Bin 433 -> 554 bytes website/scriptfan/views/articles.py | 5 +++-- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/website/scriptfan/messages.pot b/website/scriptfan/messages.pot index 03842a5..71051a4 100644 --- a/website/scriptfan/messages.pot +++ b/website/scriptfan/messages.pot @@ -13,6 +13,9 @@ msgstr "博客" msgid "articles.create" msgstr "发布文章" +msgid "articles.update" +msgstr "编辑文章" + msgid "event" msgstr "活动" @@ -30,3 +33,9 @@ msgstr "登陆" msgid "logout" msgstr "注销" + +msgid "title" +msgstr "标题" + +msgid "content" +msgstr "内容" diff --git a/website/scriptfan/templates/articles/form.html b/website/scriptfan/templates/articles/form.html index 1b966e4..8efa8b0 100644 --- a/website/scriptfan/templates/articles/form.html +++ b/website/scriptfan/templates/articles/form.html @@ -11,21 +11,21 @@

{{ self.title() }}

{{ form.hidden_tag() }}
- +
{{ form.title }} {{ form.title | error_text }}
- +
{{ form.content }} {{ form.content | error_text }}
- +
diff --git a/website/scriptfan/templates/index.html b/website/scriptfan/templates/index.html index d4fe91e..523290a 100644 --- a/website/scriptfan/templates/index.html +++ b/website/scriptfan/templates/index.html @@ -1,2 +1,2 @@ {% extends "layout.html" %} -{% block title %}{{ _("Index") }}{% endblock %} +{% block title %}{{ _("index") }}{% endblock %} diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo index 0408411985cde89484f0d0f1973e7c4e4d11b5b6..78e6add1848bf6b0942aeebe0ea53cf16b4dc630 100644 GIT binary patch literal 554 zcmYMvze~eF6bJBEty-%h3T_<=E-v-QCKV?)XD1hl>9q}{Nk}i!f`g#8ApW3Wi&cc$ zL5e~a6>6)U-24ZeoO($E-Q1jf*Pi;|n@`^5E_e4i?Jok>1SW(zz)WGf7+#A$fB?FL zPNGraIy!>=COU*}3zO(D_L|T_gV^tiJ}*2)`MM%HhhB;CH*_5Pcj1Q^XTq-VOZYAH z4fNLqP`>|+FeIEsacb|L{M>#FFFtn=gP(iF_vZKIXXfV{#f)M8N;0V;t0WD-FY~fV zBuf5!%ZTw_)-_5r3S@^r6)i?Gpy~CG#*(F0z0{6b`{uJfz|h&Qr^9X(nOWw^BI1e@Nd=ENy%YC(>UB7EL8aVDgDvV7X&(DtWJZ F0Y3tBhVK9X delta 276 zcmZ3*vXQy|o)F7a1|Z-9Vi_RL0b*Vt-UGxS@BxT9fcPU2^8@iOAZ7((E=C9~0HoP~ zd@(333#372sX+PqP}&SgYXHS', methods=['GET', 'POST']) def update(article_id): - title = u'修改文章' + title = _("articles.update") article = Article.get_by_id(article_id) form = ArticleForm(obj=article) if form.validate_on_submit(): From 78e95e2292f11e1f950f725188967ece579f502a Mon Sep 17 00:00:00 2001 From: iMom0 Date: Mon, 18 Mar 2013 23:32:22 +0800 Subject: [PATCH 065/119] Append missing submit translate --- website/scriptfan/messages.pot | 3 +++ .../translations/zh_CN/LC_MESSAGES/messages.mo | Bin 554 -> 592 bytes 2 files changed, 3 insertions(+) diff --git a/website/scriptfan/messages.pot b/website/scriptfan/messages.pot index 71051a4..33cedb8 100644 --- a/website/scriptfan/messages.pot +++ b/website/scriptfan/messages.pot @@ -39,3 +39,6 @@ msgstr "标题" msgid "content" msgstr "内容" + +msgid "submit" +msgstr "提交" diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.mo index 78e6add1848bf6b0942aeebe0ea53cf16b4dc630..8063c9654222f30cac9c4111c398021537da9f87 100644 GIT binary patch delta 361 zcmZ3*a)G7(o)F7a1|Z-AVi_Rr0b*ew{sY7y;K9hizyYK^fiypm4uaB2K$;!MF9Oo6 zK)MP_HvnliAioVt_X24yAb$#!KOagjgV6O1Yk>@q0b78y29VwbRqz-{^8op;p!9dB zI13ZR0&XZR1f?aRv;vR@=~IXDO`x@+AoH0x|bwUB(Qy;?ktt%#z7d7$@-dPk6Fx3B$7m?JpP2U;qFk C(IBA! delta 323 zcmcb>vWlhto)F7a1|Z-CVi_QA0b)TQegniHV8F=0zyYKUfiypmwt~`LK$;!Mj{?%H zKspIZX8>t7AU_XEmjY=nAioC6Z->%-KpLp8o?$AGp#fAd8!B-GNb>;sr=j!>sQ6PT z{RT>ZhSI;FG$Rv49|w>IIY1Oj%Rp&mAPrOs_9)2VEI`b_pbt^Z3}gc3!2o1A$j2a8 dgM7vb#N0p(29sSGGbV3g>}Olh{&LX_1^{Ax8Pos( From 8844a58329c41c051087e04d7dfbd75ff130ced5 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Mon, 18 Mar 2013 23:53:57 +0800 Subject: [PATCH 066/119] Fix database default Collation to utf8_unicode_ci in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 875411d..85a6aac 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ ScriptFan.com 是西安一个线下技术沙龙的官方网站程序, 沙龙的 在本地数据库中建立一个数据库,如 ``scriptfan_dev`` -**注意:** 请确保数据库、表及字段的 Collation 为 utf8 (utf8_bin) 类型 +**注意:** 请确保数据库、表及字段的 Collation 为 utf8 `utf8_unicode_ci` 类型 修改配置文件 From 152d65acb3d67fdd5af26958dc5c9c0884d70573 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Tue, 19 Mar 2013 03:23:15 +0800 Subject: [PATCH 067/119] #12 Refactor articles blurprint --- website/scriptfan/__init__.py | 32 +- website/scriptfan/messages.pot | 12 + website/scriptfan/static/css/articles.css | 22 + website/scriptfan/static/js/articles.js | 17 + .../static/lib/epiceditor/images/edit.png | Bin 0 -> 723 bytes .../lib/epiceditor/images/fullscreen.png | Bin 0 -> 492 bytes .../static/lib/epiceditor/images/preview.png | Bin 0 -> 605 bytes .../static/lib/epiceditor/js/epiceditor.js | 2141 +++++++++++++++++ .../lib/epiceditor/js/epiceditor.min.js | 4 + .../lib/epiceditor/themes/base/epiceditor.css | 31 + .../epiceditor/themes/editor/epic-dark.css | 13 + .../epiceditor/themes/editor/epic-light.css | 12 + .../lib/epiceditor/themes/preview/bartik.css | 167 ++ .../lib/epiceditor/themes/preview/github.css | 368 +++ .../themes/preview/preview-dark.css | 121 + .../scriptfan/templates/articles/_form.html | 19 + .../scriptfan/templates/articles/edit.html | 23 + .../scriptfan/templates/articles/form.html | 33 - .../scriptfan/templates/articles/index.html | 6 +- website/scriptfan/templates/articles/new.html | 23 + .../scriptfan/templates/articles/show.html | 24 + website/scriptfan/templates/enviroment.html | 1 + website/scriptfan/templates/enviroment.js | 6 + website/scriptfan/templates/layout.html | 16 +- .../zh_CN/LC_MESSAGES/messages.mo | Bin 433 -> 659 bytes website/scriptfan/views/articles.py | 30 +- website/scriptfan/views/home.py | 9 +- 27 files changed, 3058 insertions(+), 72 deletions(-) create mode 100644 website/scriptfan/static/css/articles.css create mode 100644 website/scriptfan/static/js/articles.js create mode 100644 website/scriptfan/static/lib/epiceditor/images/edit.png create mode 100644 website/scriptfan/static/lib/epiceditor/images/fullscreen.png create mode 100644 website/scriptfan/static/lib/epiceditor/images/preview.png create mode 100644 website/scriptfan/static/lib/epiceditor/js/epiceditor.js create mode 100644 website/scriptfan/static/lib/epiceditor/js/epiceditor.min.js create mode 100644 website/scriptfan/static/lib/epiceditor/themes/base/epiceditor.css create mode 100644 website/scriptfan/static/lib/epiceditor/themes/editor/epic-dark.css create mode 100644 website/scriptfan/static/lib/epiceditor/themes/editor/epic-light.css create mode 100644 website/scriptfan/static/lib/epiceditor/themes/preview/bartik.css create mode 100644 website/scriptfan/static/lib/epiceditor/themes/preview/github.css create mode 100644 website/scriptfan/static/lib/epiceditor/themes/preview/preview-dark.css create mode 100644 website/scriptfan/templates/articles/_form.html create mode 100644 website/scriptfan/templates/articles/edit.html delete mode 100644 website/scriptfan/templates/articles/form.html create mode 100644 website/scriptfan/templates/articles/new.html create mode 100644 website/scriptfan/templates/articles/show.html create mode 100644 website/scriptfan/templates/enviroment.html create mode 100644 website/scriptfan/templates/enviroment.js diff --git a/website/scriptfan/__init__.py b/website/scriptfan/__init__.py index ade01fe..ba3a27a 100644 --- a/website/scriptfan/__init__.py +++ b/website/scriptfan/__init__.py @@ -52,29 +52,29 @@ def after_request(response): def dispatch_handlers(app): d = {} - @app.errorhandler(403) - def permission_error(error): - d['title'] = u'您没有权限' - d['message'] = u'您没有权限执行当前的操作, 请登陆或检查url是否错误.' - return render_template('error.html', **d), 403 + # @app.errorhandler(403) + # def permission_error(error): + # d['title'] = u'您没有权限' + # d['message'] = u'您没有权限执行当前的操作, 请登陆或检查url是否错误.' + # return render_template('error.html', **d), 403 - @app.errorhandler(404) - def page_not_found(error): - d['title'] = u'页面不存在' - d['message'] = u'您所访问的页面不存在, 是不是打错地址了啊?' - return render_template('error.html', **d), 404 + # @app.errorhandler(404) + # def page_not_found(error): + # d['title'] = u'页面不存在' + # d['message'] = u'您所访问的页面不存在, 是不是打错地址了啊?' + # return render_template('error.html', **d), 404 - @app.errorhandler(500) - def page_error(error): - d['title'] = u'页面出错啦' - d['message'] = u'您所访问的页面出错啦! 待会再来吧!' - return render_template('error.html', **d), 500 + # @app.errorhandler(500) + # def page_error(error): + # d['title'] = u'页面出错啦' + # d['message'] = u'您所访问的页面出错啦! 待会再来吧!' + # return render_template('error.html', **d), 500 def register_blueprints(app): app.logger.info('Register blueprints...') from scriptfan.views import home, events, users, articles - app.register_blueprint(home.blueprint, url_prefix='/') + app.register_blueprint(home.blueprint, url_prefix='') app.register_blueprint(users.blurprint, url_prefix='/users') app.register_blueprint(events.blueprint, url_prefix='/events') app.register_blueprint(articles.blueprint, url_prefix='/articles') diff --git a/website/scriptfan/messages.pot b/website/scriptfan/messages.pot index 03842a5..8c793bf 100644 --- a/website/scriptfan/messages.pot +++ b/website/scriptfan/messages.pot @@ -10,9 +10,21 @@ msgstr "首页" msgid "articles" msgstr "博客" +msgid "models.article.title" +msgstr "标题" + msgid "articles.create" msgstr "发布文章" +msgid "articles.edit" +msgstr "编辑文章" + +msgid "views.articles.edit.title" +msgstr "编辑文章:%%s" + +msgid "views.articles.new.title" +msgstr "发布文章" + msgid "event" msgstr "活动" diff --git a/website/scriptfan/static/css/articles.css b/website/scriptfan/static/css/articles.css new file mode 100644 index 0000000..7518314 --- /dev/null +++ b/website/scriptfan/static/css/articles.css @@ -0,0 +1,22 @@ +.article-list { + list-style-type: none; + margin-left: 0; + padding-left: 0; +} + +#epiceditor { + width: 100%; + height: 349px; + border-radius: 3px; + background-color: #fff; + margin-bottom: 9px; + border: 1px solid #ccc; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075); + box-shadow: inset 0 1px 1px rgba(0,0,0,0.075); + -webkit-transition: border linear .2s,box-shadow linear .2s; + -moz-transition: border linear .2s,box-shadow linear .2s; + -o-transition: border linear .2s,box-shadow linear .2s; + transition: border linear .2s,box-shadow linear .2s; +} + diff --git a/website/scriptfan/static/js/articles.js b/website/scriptfan/static/js/articles.js new file mode 100644 index 0000000..9212c83 --- /dev/null +++ b/website/scriptfan/static/js/articles.js @@ -0,0 +1,17 @@ +$(function() { + var editor = new EpicEditor({ + basePath: ENV.STATIC + 'lib/epiceditor', + theme: { + base:'/themes/base/epiceditor.css', + preview:'/themes/preview/preview-dark.css', + editor:'/themes/editor/epic-light.css' + } + }).load(); + editor.importFile('post', $('#content').val()); + + $('#form-article-create').submit(function() { + var content = editor.exportFile(); + $('#content').val(content); + console.log(content); + }); +}); diff --git a/website/scriptfan/static/lib/epiceditor/images/edit.png b/website/scriptfan/static/lib/epiceditor/images/edit.png new file mode 100644 index 0000000000000000000000000000000000000000..ebb9e939ac72870508b60f8f3f01d5cd930738cc GIT binary patch literal 723 zcmV;^0xbQBP)w#+Br1hQA!L$@9Epbh?B)T-yZSah!`p{2jj$8$B!HBM8Vw)42A}PBkP; zFoCbaKuGvQ*bqvxLT4FvU6-S9U~r#w#PtRa9dCHCSXdxR_Jle=fMAJrPU-a{gPg)8 z!8(y#Y%D`~K{-~;?sB;tB^sX3=bX;RJyAxYwn_4X@Egt>f(OM)r4oE-r`c>~k`rq} zmYvbCA>;^N%rhOlr=+d1&NbV37x8pD<%P8tjf}2{l)Zcsae%$gBGRm;vrw&8gVba) zVMR~ch8~nW;y#1tPoJdO2BVeX0^aKg{p?Kch9X{y^oE>L@~BI{kw{fq+y@CM=I5N1 zw6^Y}Ouk6_k`3o&eQLE@fN3QSpudw${OtfO7(TLvlF*zVVX{skJK@28cH{tVV2q!~KFGqAij|_GbC)aQ|%tJxa z5D5{9`Vw)A+{Sc<%!b`;HiNw*Q^{a3=xQD(3D4r6qV;6Ya(Vn#t9AH6fHreW-4c3& zd~tZtCRG3uW>gCu^pS{@e!p*kR9x?Gf`lQ`%+dZr2|j!d+wJyQCy)S$gkevN)W3h& z)3V}d^}WdPHZPd#Bl>@LycH3bv{S~(lKTG_=2G%1zyOLXMvT literal 0 HcmV?d00001 diff --git a/website/scriptfan/static/lib/epiceditor/images/fullscreen.png b/website/scriptfan/static/lib/epiceditor/images/fullscreen.png new file mode 100644 index 0000000000000000000000000000000000000000..04cf59ee5f777f36bea39878125cb45fe992122e GIT binary patch literal 492 zcmVWJ`b&T*8*H9gs0Hk%M4t6BPE4kw(VW%$tv&4Vcq38CS@MagkgW z*b*mw-(!7uT-P0CfEWh&o9|VzKr|neu576aAYeGFB?8)_fyE%6=Z!}yS^ITer&Y1h zwyh1ru$LIybN)Q=sy2C{+iFbapg|D4*raJ1M-u$Kj+41pWnI^&v^osKQQB&awi6*K za_Y^+0&}IHpL`V830Kti=pN54jiTtuTT4%}MJr}mGoYEhY=rrJRiUW&m94`t4!3%d zs;X`{Wy*fu^MA9-B?#Y!AbS-Qah0000f}5|H?i_$Z$Ofgs5Q&<*GWr4x`1nh`Pq^c^}Q*CHU4s{PSeI@vy-{q7z13%S$j z^z;`kmjP$M{}VU}0s~zYM!lI3wugnVSI2R}RKULPTj*Y3`h$cb*wA&|?L)u-BxW0C zn5GeP8wrGWfj!R~Fw388H8ejFc)eZ|ODkdXj{9=LI+ZnlYrS5t+hU@L!D_YIAPI{S zxnebJs-Vv&tdr>+Q%hL42;&>$<#MSqjhTVV^CJ5`@07H_tPeaN!k(3vux&INwzgO- zEQRR~R)D=pzK9szV0&TBv0}alNQtWu3+9BWVc^LnZ8n<~SS$sS8Sw3Syrl!L0dtHY z`KU_d_c1OWk4KS|rzyXQGoR1TPpH*ug%UW42lf7qd*E|SRe;hN2x{$i zn;29WL`PPK*=*+WAv;kX_zc9>7~E7rX^mo}#Llk(K8iiCpD-$fd3jFL=~NYskT1uE zFASBf)ai6W#OjioMes$u|6PuSInt#)iwW(&Dj<`|L=}*j54#8(>2bH)4LM%uxT{E} zEVDzz5AI6lcs$mbshLu-=)+<6dcEkOkHly+GPzvk2Lb8#4f_55KGkOt3Fr(i#2~MP r#LVu4KmizK|3EU}4EWdDmjD9*d>2aZZ#9pm00000NkvXXu0mjfRFMyo literal 0 HcmV?d00001 diff --git a/website/scriptfan/static/lib/epiceditor/js/epiceditor.js b/website/scriptfan/static/lib/epiceditor/js/epiceditor.js new file mode 100644 index 0000000..c53d89a --- /dev/null +++ b/website/scriptfan/static/lib/epiceditor/js/epiceditor.js @@ -0,0 +1,2141 @@ +/** + * EpicEditor - An Embeddable JavaScript Markdown Editor (https://github.com/OscarGodson/EpicEditor) + * Copyright (c) 2011-2012, Oscar Godson. (MIT Licensed) + */ + +(function (window, undefined) { + /** + * Applies attributes to a DOM object + * @param {object} context The DOM obj you want to apply the attributes to + * @param {object} attrs A key/value pair of attributes you want to apply + * @returns {undefined} + */ + function _applyAttrs(context, attrs) { + for (var attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + context[attr] = attrs[attr]; + } + } + } + + /** + * Applies styles to a DOM object + * @param {object} context The DOM obj you want to apply the attributes to + * @param {object} attrs A key/value pair of attributes you want to apply + * @returns {undefined} + */ + function _applyStyles(context, attrs) { + for (var attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + context.style[attr] = attrs[attr]; + } + } + } + + /** + * Returns a DOM objects computed style + * @param {object} el The element you want to get the style from + * @param {string} styleProp The property you want to get from the element + * @returns {string} Returns a string of the value. If property is not set it will return a blank string + */ + function _getStyle(el, styleProp) { + var x = el + , y = null; + if (window.getComputedStyle) { + y = document.defaultView.getComputedStyle(x, null).getPropertyValue(styleProp); + } + else if (x.currentStyle) { + y = x.currentStyle[styleProp]; + } + return y; + } + + /** + * Saves the current style state for the styles requested, then applys styles + * to overwrite the existing one. The old styles are returned as an object so + * you can pass it back in when you want to revert back to the old style + * @param {object} el The element to get the styles of + * @param {string} type Can be "save" or "apply". apply will just apply styles you give it. Save will write styles + * @param {object} styles Key/value style/property pairs + * @returns {object} + */ + function _saveStyleState(el, type, styles) { + var returnState = {} + , style; + if (type === 'save') { + for (style in styles) { + if (styles.hasOwnProperty(style)) { + returnState[style] = _getStyle(el, style); + } + } + // After it's all done saving all the previous states, change the styles + _applyStyles(el, styles); + } + else if (type === 'apply') { + _applyStyles(el, styles); + } + return returnState; + } + + /** + * Gets an elements total width including it's borders and padding + * @param {object} el The element to get the total width of + * @returns {int} + */ + function _outerWidth(el) { + var b = parseInt(_getStyle(el, 'border-left-width'), 10) + parseInt(_getStyle(el, 'border-right-width'), 10) + , p = parseInt(_getStyle(el, 'padding-left'), 10) + parseInt(_getStyle(el, 'padding-right'), 10) + , w = el.offsetWidth + , t; + // For IE in case no border is set and it defaults to "medium" + if (isNaN(b)) { b = 0; } + t = b + p + w; + return t; + } + + /** + * Gets an elements total height including it's borders and padding + * @param {object} el The element to get the total width of + * @returns {int} + */ + function _outerHeight(el) { + var b = parseInt(_getStyle(el, 'border-top-width'), 10) + parseInt(_getStyle(el, 'border-bottom-width'), 10) + , p = parseInt(_getStyle(el, 'padding-top'), 10) + parseInt(_getStyle(el, 'padding-bottom'), 10) + , w = el.offsetHeight + , t; + // For IE in case no border is set and it defaults to "medium" + if (isNaN(b)) { b = 0; } + t = b + p + w; + return t; + } + + /** + * Inserts a tag specifically for CSS + * @param {string} path The path to the CSS file + * @param {object} context In what context you want to apply this to (document, iframe, etc) + * @param {string} id An id for you to reference later for changing properties of the + * @returns {undefined} + */ + function _insertCSSLink(path, context, id) { + id = id || ''; + var headID = context.getElementsByTagName("head")[0] + , cssNode = context.createElement('link'); + + _applyAttrs(cssNode, { + type: 'text/css' + , id: id + , rel: 'stylesheet' + , href: path + , name: path + , media: 'screen' + }); + + headID.appendChild(cssNode); + } + + // Simply replaces a class (o), to a new class (n) on an element provided (e) + function _replaceClass(e, o, n) { + e.className = e.className.replace(o, n); + } + + // Feature detects an iframe to get the inner document for writing to + function _getIframeInnards(el) { + return el.contentDocument || el.contentWindow.document; + } + + // Grabs the text from an element and preserves whitespace + function _getText(el) { + var theText; + // Make sure to check for type of string because if the body of the page + // doesn't have any text it'll be "" which is falsey and will go into + // the else which is meant for Firefox and shit will break + if (typeof document.body.innerText == 'string') { + theText = el.innerText; + } + else { + // First replace
s before replacing the rest of the HTML + theText = el.innerHTML.replace(/
/gi, "\n"); + // Now we can clean the HTML + theText = theText.replace(/<(?:.|\n)*?>/gm, ''); + // Now fix HTML entities + theText = theText.replace(/</gi, '<'); + theText = theText.replace(/>/gi, '>'); + } + return theText; + } + + function _setText(el, content) { + // If you want to know why we check for typeof string, see comment + // in the _getText function + if (typeof document.body.innerText == 'string') { + content = content.replace(/ /g, '\u00a0'); + el.innerText = content; + } + else { + // Don't convert lt/gt characters as HTML when viewing the editor window + // TODO: Write a test to catch regressions for this + content = content.replace(//g, '>'); + content = content.replace(/\n/g, '
'); + // Make sure to look for TWO spaces and replace with a space and   + // If you find and replace every space with a   text will not wrap. + // Hence the name (Non-Breaking-SPace). + content = content.replace(/\s\s/g, '  ') + el.innerHTML = content; + } + return true; + } + + /** + * Will return the version number if the browser is IE. If not will return -1 + * TRY NEVER TO USE THIS AND USE FEATURE DETECTION IF POSSIBLE + * @returns {Number} -1 if false or the version number if true + */ + function _isIE() { + var rv = -1 // Return value assumes failure. + , ua = navigator.userAgent + , re; + if (navigator.appName == 'Microsoft Internet Explorer') { + re = /MSIE ([0-9]{1,}[\.0-9]{0,})/; + if (re.exec(ua) != null) { + rv = parseFloat(RegExp.$1, 10); + } + } + return rv; + } + + /** + * Same as the isIE(), but simply returns a boolean + * THIS IS TERRIBLE AND IS ONLY USED BECAUSE FULLSCREEN IN SAFARI IS BORKED + * If some other engine uses WebKit and has support for fullscreen they + * probably wont get native fullscreen until Safari's fullscreen is fixed + * @returns {Boolean} true if Safari + */ + function _isSafari() { + var n = window.navigator; + return n.userAgent.indexOf('Safari') > -1 && n.userAgent.indexOf('Chrome') == -1; + } + + /** + * Determines if supplied value is a function + * @param {object} object to determine type + */ + function _isFunction(functionToCheck) { + var getType = {}; + return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; + } + + /** + * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1 + * @param {boolean} [deepMerge=false] If true, will deep merge meaning it will merge sub-objects like {obj:obj2{foo:'bar'}} + * @param {object} first object + * @param {object} second object + * @returnss {object} a new object based on obj1 and obj2 + */ + function _mergeObjs() { + // copy reference to target object + var target = arguments[0] || {} + , i = 1 + , length = arguments.length + , deep = false + , options + , name + , src + , copy + + // Handle a deep copy situation + if (typeof target === "boolean") { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if (typeof target !== "object" && !_isFunction(target)) { + target = {}; + } + // extend jQuery itself if only one argument is passed + if (length === i) { + target = this; + --i; + } + + for (; i < length; i++) { + // Only deal with non-null/undefined values + if ((options = arguments[i]) != null) { + // Extend the base object + for (name in options) { + // @NOTE: added hasOwnProperty check + if (options.hasOwnProperty(name)) { + src = target[name]; + copy = options[name]; + // Prevent never-ending loop + if (target === copy) { + continue; + } + // Recurse if we're merging object values + if (deep && copy && typeof copy === "object" && !copy.nodeType) { + target[name] = _mergeObjs(deep, + // Never move original objects, clone them + src || (copy.length != null ? [] : {}) + , copy); + } else if (copy !== undefined) { // Don't bring in undefined values + target[name] = copy; + } + } + } + } + } + + // Return the modified object + return target; + } + + /** + * Initiates the EpicEditor object and sets up offline storage as well + * @class Represents an EpicEditor instance + * @param {object} options An optional customization object + * @returns {object} EpicEditor will be returned + */ + function EpicEditor(options) { + // Default settings will be overwritten/extended by options arg + var self = this + , opts = options || {} + , _defaultFileSchema + , _defaultFile + , defaults = { container: 'epiceditor' + , basePath: 'epiceditor' + , clientSideStorage: true + , localStorageName: 'epiceditor' + , useNativeFullscreen: true + , file: { name: null + , defaultContent: '' + , autoSave: 100 // Set to false for no auto saving + } + , theme: { base: '/themes/base/epiceditor.css' + , preview: '/themes/preview/github.css' + , editor: '/themes/editor/epic-dark.css' + } + , focusOnLoad: false + , shortcut: { modifier: 18 // alt keycode + , fullscreen: 70 // f keycode + , preview: 80 // p keycode + } + , parser: typeof marked == 'function' ? marked : null + } + , defaultStorage; + + self.settings = _mergeObjs(true, defaults, opts); + + if (!(typeof self.settings.parser == 'function' && typeof self.settings.parser('TEST') == 'string')) { + self.settings.parser = function (str) { + return str; + } + } + + + // Grab the container element and save it to self.element + // if it's a string assume it's an ID and if it's an object + // assume it's a DOM element + if (typeof self.settings.container == 'string') { + self.element = document.getElementById(self.settings.container); + } + else if (typeof self.settings.container == 'object') { + self.element = self.settings.container; + } + + // Figure out the file name. If no file name is given we'll use the ID. + // If there's no ID either we'll use a namespaced file name that's incremented + // based on the calling order. As long as it doesn't change, drafts will be saved. + if (!self.settings.file.name) { + if (typeof self.settings.container == 'string') { + self.settings.file.name = self.settings.container; + } + else if (typeof self.settings.container == 'object') { + if (self.element.id) { + self.settings.file.name = self.element.id; + } + else { + if (!EpicEditor._data.unnamedEditors) { + EpicEditor._data.unnamedEditors = []; + } + EpicEditor._data.unnamedEditors.push(self); + self.settings.file.name = '__epiceditor-untitled-' + EpicEditor._data.unnamedEditors.length; + } + } + } + + // Protect the id and overwrite if passed in as an option + // TODO: Put underscrore to denote that this is private + self._instanceId = 'epiceditor-' + Math.round(Math.random() * 100000); + self._storage = {}; + self._canSave = true; + + // Setup local storage of files + self._defaultFileSchema = function () { + return { + content: self.settings.file.defaultContent + , created: new Date() + , modified: new Date() + } + } + + if (localStorage && self.settings.clientSideStorage) { + this._storage = localStorage; + if (this._storage[self.settings.localStorageName] && self.getFiles(self.settings.file.name) === undefined) { + _defaultFile = self.getFiles(self.settings.file.name); + _defaultFile = self._defaultFileSchema(); + _defaultFile.content = self.settings.file.defaultContent; + } + } + + if (!this._storage[self.settings.localStorageName]) { + defaultStorage = {}; + defaultStorage[self.settings.file.name] = self._defaultFileSchema(); + defaultStorage = JSON.stringify(defaultStorage); + this._storage[self.settings.localStorageName] = defaultStorage; + } + + // This needs to replace the use of classes to check the state of EE + self._eeState = { + fullscreen: false + , preview: false + , edit: false + , loaded: false + , unloaded: false + } + + // Now that it exists, allow binding of events if it doesn't exist yet + if (!self.events) { + self.events = {}; + } + + return this; + } + + /** + * Inserts the EpicEditor into the DOM via an iframe and gets it ready for editing and previewing + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.load = function (callback) { + + // Get out early if it's already loaded + if (this.is('loaded')) { return this; } + + // TODO: Gotta get the privates with underscores! + // TODO: Gotta document what these are for... + var self = this + , _HtmlTemplates + , iframeElement + , baseTag + , utilBtns + , utilBar + , utilBarTimer + , keypressTimer + , mousePos = { y: -1, x: -1 } + , _elementStates + , _isInEdit + , nativeFs = false + , fsElement + , isMod = false + , isCtrl = false + , eventableIframes + , i; // i is reused for loops + + if (self.settings.useNativeFullscreen) { + nativeFs = document.body.webkitRequestFullScreen ? true : false + } + + // Fucking Safari's native fullscreen works terribly + // REMOVE THIS IF SAFARI 7 WORKS BETTER + if (_isSafari()) { + nativeFs = false; + } + + // It opens edit mode by default (for now); + if (!self.is('edit') && !self.is('preview')) { + self._eeState.edit = true; + } + + callback = callback || function () {}; + + // The editor HTML + // TODO: edit-mode class should be dynamically added + _HtmlTemplates = { + // This is wrapping iframe element. It contains the other two iframes and the utilbar + chrome: '
' + + '' + + '' + + '
' + + ' ' + + ' ' + + '' + + '
' + + '
' + + // The previewer is just an empty box for the generated HTML to go into + , previewer: '
' + }; + + // Write an iframe and then select it for the editor + self.element.innerHTML = ''; + + // Because browsers add things like invisible padding and margins and stuff + // to iframes, we need to set manually set the height so that the height + // doesn't keep increasing (by 2px?) every time reflow() is called. + // FIXME: Figure out how to fix this without setting this + self.element.style.height = self.element.offsetHeight + 'px'; + + iframeElement = document.getElementById(self._instanceId); + + // Store a reference to the iframeElement itself + self.iframeElement = iframeElement; + + // Grab the innards of the iframe (returns the document.body) + // TODO: Change self.iframe to self.iframeDocument + self.iframe = _getIframeInnards(iframeElement); + self.iframe.open(); + self.iframe.write(_HtmlTemplates.chrome); + + // Now that we got the innards of the iframe, we can grab the other iframes + self.editorIframe = self.iframe.getElementById('epiceditor-editor-frame') + self.previewerIframe = self.iframe.getElementById('epiceditor-previewer-frame'); + + // Setup the editor iframe + self.editorIframeDocument = _getIframeInnards(self.editorIframe); + self.editorIframeDocument.open(); + // Need something for... you guessed it, Firefox + self.editorIframeDocument.write(''); + self.editorIframeDocument.close(); + + // Setup the previewer iframe + self.previewerIframeDocument = _getIframeInnards(self.previewerIframe); + self.previewerIframeDocument.open(); + self.previewerIframeDocument.write(_HtmlTemplates.previewer); + + // Base tag is added so that links will open a new tab and not inside of the iframes + baseTag = self.previewerIframeDocument.createElement('base'); + baseTag.target = '_blank'; + self.previewerIframeDocument.getElementsByTagName('head')[0].appendChild(baseTag); + + self.previewerIframeDocument.close(); + + self.reflow(); + + // Insert Base Stylesheet + _insertCSSLink(self.settings.basePath + self.settings.theme.base, self.iframe, 'theme'); + + // Insert Editor Stylesheet + _insertCSSLink(self.settings.basePath + self.settings.theme.editor, self.editorIframeDocument, 'theme'); + + // Insert Previewer Stylesheet + _insertCSSLink(self.settings.basePath + self.settings.theme.preview, self.previewerIframeDocument, 'theme'); + + // Add a relative style to the overall wrapper to keep CSS relative to the editor + self.iframe.getElementById('epiceditor-wrapper').style.position = 'relative'; + + // Now grab the editor and previewer for later use + self.editor = self.editorIframeDocument.body; + self.previewer = self.previewerIframeDocument.getElementById('epiceditor-preview'); + + self.editor.contentEditable = true; + + // Firefox's gets all fucked up so, to be sure, we need to hardcode it + self.iframe.body.style.height = this.element.offsetHeight + 'px'; + + // Should actually check what mode it's in! + this.previewerIframe.style.display = 'none'; + + // FIXME figure out why it needs +2 px + if (_isIE() > -1) { + this.previewer.style.height = parseInt(_getStyle(this.previewer, 'height'), 10) + 2; + } + + // If there is a file to be opened with that filename and it has content... + this.open(self.settings.file.name); + + if (self.settings.focusOnLoad) { + // We need to wait until all three iframes are done loading by waiting until the parent + // iframe's ready state == complete, then we can focus on the contenteditable + self.iframe.addEventListener('readystatechange', function () { + if (self.iframe.readyState == 'complete') { + self.editorIframeDocument.body.focus(); + } + }); + } + + utilBtns = self.iframe.getElementById('epiceditor-utilbar'); + + _elementStates = {} + self._goFullscreen = function (el) { + + if (self.is('fullscreen')) { + self._exitFullscreen(el); + return; + } + + if (nativeFs) { + el.webkitRequestFullScreen(); + } + + _isInEdit = self.is('edit'); + + // Set the state of EE in fullscreen + // We set edit and preview to true also because they're visible + // we might want to allow fullscreen edit mode without preview (like a "zen" mode) + self._eeState.fullscreen = true; + self._eeState.edit = true; + self._eeState.preview = true; + + // Cache calculations + var windowInnerWidth = window.innerWidth + , windowInnerHeight = window.innerHeight + , windowOuterWidth = window.outerWidth + , windowOuterHeight = window.outerHeight; + + // Without this the scrollbars will get hidden when scrolled to the bottom in faux fullscreen (see #66) + if (!nativeFs) { + windowOuterHeight = window.innerHeight; + } + + // This MUST come first because the editor is 100% width so if we change the width of the iframe or wrapper + // the editor's width wont be the same as before + _elementStates.editorIframe = _saveStyleState(self.editorIframe, 'save', { + 'width': windowOuterWidth / 2 + 'px' + , 'height': windowOuterHeight + 'px' + , 'float': 'left' // Most browsers + , 'cssFloat': 'left' // FF + , 'styleFloat': 'left' // Older IEs + , 'display': 'block' + }); + + // the previewer + _elementStates.previewerIframe = _saveStyleState(self.previewerIframe, 'save', { + 'width': windowOuterWidth / 2 + 'px' + , 'height': windowOuterHeight + 'px' + , 'float': 'right' // Most browsers + , 'cssFloat': 'right' // FF + , 'styleFloat': 'right' // Older IEs + , 'display': 'block' + }); + + // Setup the containing element CSS for fullscreen + _elementStates.element = _saveStyleState(self.element, 'save', { + 'position': 'fixed' + , 'top': '0' + , 'left': '0' + , 'width': '100%' + , 'z-index': '9999' // Most browsers + , 'zIndex': '9999' // Firefox + , 'border': 'none' + , 'margin': '0' + // Should use the base styles background! + , 'background': _getStyle(self.editor, 'background-color') // Try to hide the site below + , 'height': windowInnerHeight + 'px' + }); + + // The iframe element + _elementStates.iframeElement = _saveStyleState(self.iframeElement, 'save', { + 'width': windowOuterWidth + 'px' + , 'height': windowInnerHeight + 'px' + }); + + // ...Oh, and hide the buttons and prevent scrolling + utilBtns.style.visibility = 'hidden'; + + if (!nativeFs) { + document.body.style.overflow = 'hidden'; + } + + self.preview(); + + self.editorIframeDocument.body.focus(); + + self.emit('fullscreenenter'); + }; + + self._exitFullscreen = function (el) { + _saveStyleState(self.element, 'apply', _elementStates.element); + _saveStyleState(self.iframeElement, 'apply', _elementStates.iframeElement); + _saveStyleState(self.editorIframe, 'apply', _elementStates.editorIframe); + _saveStyleState(self.previewerIframe, 'apply', _elementStates.previewerIframe); + + // We want to always revert back to the original styles in the CSS so, + // if it's a fluid width container it will expand on resize and not get + // stuck at a specific width after closing fullscreen. + self.element.style.width = self._eeState.reflowWidth ? self._eeState.reflowWidth : ''; + self.element.style.height = self._eeState.reflowHeight ? self._eeState.reflowHeight : ''; + + utilBtns.style.visibility = 'visible'; + + if (!nativeFs) { + document.body.style.overflow = 'auto'; + } + else { + document.webkitCancelFullScreen(); + } + // Put the editor back in the right state + // TODO: This is ugly... how do we make this nicer? + self._eeState.fullscreen = false; + + if (_isInEdit) { + self.edit(); + } + else { + self.preview(); + } + + self.reflow(); + + self.emit('fullscreenexit'); + }; + + // This setups up live previews by triggering preview() IF in fullscreen on keyup + self.editor.addEventListener('keyup', function () { + if (keypressTimer) { + window.clearTimeout(keypressTimer); + } + keypressTimer = window.setTimeout(function () { + if (self.is('fullscreen')) { + self.preview(); + } + }, 250); + }); + + fsElement = self.iframeElement; + + // Sets up the onclick event on utility buttons + utilBtns.addEventListener('click', function (e) { + var targetClass = e.target.className; + if (targetClass.indexOf('epiceditor-toggle-preview-btn') > -1) { + self.preview(); + } + else if (targetClass.indexOf('epiceditor-toggle-edit-btn') > -1) { + self.edit(); + } + else if (targetClass.indexOf('epiceditor-fullscreen-btn') > -1) { + self._goFullscreen(fsElement); + } + }); + + // Sets up the NATIVE fullscreen editor/previewer for WebKit + if (document.body.webkitRequestFullScreen) { + fsElement.addEventListener('webkitfullscreenchange', function () { + if (!document.webkitIsFullScreen) { + self._exitFullscreen(fsElement); + } + }, false); + } + + utilBar = self.iframe.getElementById('epiceditor-utilbar'); + + // Hide it at first until they move their mouse + utilBar.style.display = 'none'; + + utilBar.addEventListener('mouseover', function () { + if (utilBarTimer) { + clearTimeout(utilBarTimer); + } + }); + + function utilBarHandler(e) { + // Here we check if the mouse has moves more than 5px in any direction before triggering the mousemove code + // we do this for 2 reasons: + // 1. On Mac OS X lion when you scroll and it does the iOS like "jump" when it hits the top/bottom of the page itll fire off + // a mousemove of a few pixels depending on how hard you scroll + // 2. We give a slight buffer to the user in case he barely touches his touchpad or mouse and not trigger the UI + if (Math.abs(mousePos.y - e.pageY) >= 5 || Math.abs(mousePos.x - e.pageX) >= 5) { + utilBar.style.display = 'block'; + // if we have a timer already running, kill it out + if (utilBarTimer) { + clearTimeout(utilBarTimer); + } + + // begin a new timer that hides our object after 1000 ms + utilBarTimer = window.setTimeout(function () { + utilBar.style.display = 'none'; + }, 1000); + } + mousePos = { y: e.pageY, x: e.pageX }; + } + + // Add keyboard shortcuts for convenience. + function shortcutHandler(e) { + if (e.keyCode == self.settings.shortcut.modifier) { isMod = true } // check for modifier press(default is alt key), save to var + if (e.keyCode == 17) { isCtrl = true } // check for ctrl/cmnd press, in order to catch ctrl/cmnd + s + + // Check for alt+p and make sure were not in fullscreen - default shortcut to switch to preview + if (isMod === true && e.keyCode == self.settings.shortcut.preview && !self.is('fullscreen')) { + e.preventDefault(); + if (self.is('edit')) { + self.preview(); + } + else { + self.edit(); + } + } + // Check for alt+f - default shortcut to make editor fullscreen + if (isMod === true && e.keyCode == self.settings.shortcut.fullscreen) { + e.preventDefault(); + self._goFullscreen(fsElement); + } + + // Set the modifier key to false once *any* key combo is completed + // or else, on Windows, hitting the alt key will lock the isMod state to true (ticket #133) + if (isMod === true && e.keyCode !== self.settings.shortcut.modifier) { + isMod = false; + } + + // When a user presses "esc", revert everything! + if (e.keyCode == 27 && self.is('fullscreen')) { + self._exitFullscreen(fsElement); + } + + // Check for ctrl + s (since a lot of people do it out of habit) and make it do nothing + if (isCtrl === true && e.keyCode == 83) { + self.save(); + e.preventDefault(); + isCtrl = false; + } + + // Do the same for Mac now (metaKey == cmd). + if (e.metaKey && e.keyCode == 83) { + self.save(); + e.preventDefault(); + } + + } + + function shortcutUpHandler(e) { + if (e.keyCode == self.settings.shortcut.modifier) { isMod = false } + if (e.keyCode == 17) { isCtrl = false } + } + + // Hide and show the util bar based on mouse movements + eventableIframes = [self.previewerIframeDocument, self.editorIframeDocument]; + + for (i = 0; i < eventableIframes.length; i++) { + eventableIframes[i].addEventListener('mousemove', function (e) { + utilBarHandler(e); + }); + eventableIframes[i].addEventListener('scroll', function (e) { + utilBarHandler(e); + }); + eventableIframes[i].addEventListener('keyup', function (e) { + shortcutUpHandler(e); + }); + eventableIframes[i].addEventListener('keydown', function (e) { + shortcutHandler(e); + }); + } + + // Save the document every 100ms by default + if (self.settings.file.autoSave) { + self.saveInterval = window.setInterval(function () { + if (!self._canSave) { + return; + } + self.save(); + }, self.settings.file.autoSave); + } + + window.addEventListener('resize', function () { + // If NOT webkit, and in fullscreen, we need to account for browser resizing + // we don't care about webkit because you can't resize in webkit's fullscreen + if (!self.iframe.webkitRequestFullScreen && self.is('fullscreen')) { + _applyStyles(self.iframeElement, { + 'width': window.outerWidth + 'px' + , 'height': window.innerHeight + 'px' + }); + + _applyStyles(self.element, { + 'height': window.innerHeight + 'px' + }); + + _applyStyles(self.previewerIframe, { + 'width': window.outerWidth / 2 + 'px' + , 'height': window.innerHeight + 'px' + }); + + _applyStyles(self.editorIframe, { + 'width': window.outerWidth / 2 + 'px' + , 'height': window.innerHeight + 'px' + }); + } + // Makes the editor support fluid width when not in fullscreen mode + else if (!self.is('fullscreen')) { + self.reflow(); + } + }); + + // Set states before flipping edit and preview modes + self._eeState.loaded = true; + self._eeState.unloaded = false; + + if (self.is('preview')) { + self.preview(); + } + else { + self.edit(); + } + + self.iframe.close(); + // The callback and call are the same thing, but different ways to access them + callback.call(this); + this.emit('load'); + return this; + } + + /** + * Will remove the editor, but not offline files + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.unload = function (callback) { + + // Make sure the editor isn't already unloaded. + if (this.is('unloaded')) { + throw new Error('Editor isn\'t loaded'); + } + + var self = this + , editor = window.parent.document.getElementById(self._instanceId); + + editor.parentNode.removeChild(editor); + self._eeState.loaded = false; + self._eeState.unloaded = true; + callback = callback || function () {}; + + if (self.saveInterval) { + window.clearInterval(self.saveInterval); + } + + callback.call(this); + self.emit('unload'); + return self; + } + + /** + * reflow allows you to dynamically re-fit the editor in the parent without + * having to unload and then reload the editor again. + * + * @param {string} kind Can either be 'width' or 'height' or null + * if null, both the height and width will be resized + * + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.reflow = function (kind) { + var self = this + , widthDiff = _outerWidth(self.element) - self.element.offsetWidth + , heightDiff = _outerHeight(self.element) - self.element.offsetHeight + , elements = [self.iframeElement, self.editorIframe, self.previewerIframe] + , newWidth + , newHeight; + + + for (var x = 0; x < elements.length; x++) { + if (!kind || kind == 'width') { + newWidth = self.element.offsetWidth - widthDiff + 'px'; + elements[x].style.width = newWidth; + self._eeState.reflowWidth = newWidth; + } + if (!kind || kind == 'height') { + newHeight = self.element.offsetHeight - heightDiff + 'px'; + elements[x].style.height = newHeight; + self._eeState.reflowHeight = newHeight + } + } + return self; + } + + /** + * Will take the markdown and generate a preview view based on the theme + * @param {string} theme The path to the theme you want to preview in + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.preview = function (theme) { + var self = this + , x + , anchors; + + theme = theme || self.settings.basePath + self.settings.theme.preview; + + _replaceClass(self.getElement('wrapper'), 'epiceditor-edit-mode', 'epiceditor-preview-mode'); + + // Check if no CSS theme link exists + if (!self.previewerIframeDocument.getElementById('theme')) { + _insertCSSLink(theme, self.previewerIframeDocument, 'theme'); + } + else if (self.previewerIframeDocument.getElementById('theme').name !== theme) { + self.previewerIframeDocument.getElementById('theme').href = theme; + } + + // Add the generated HTML into the previewer + self.previewer.innerHTML = self.exportFile(null, 'html'); + + // Because we have a tag so all links open in a new window we + // need to prevent hash links from opening in a new window + anchors = self.previewer.getElementsByTagName('a'); + for (x in anchors) { + // If the link is a hash AND the links hostname is the same as the + // current window's hostname (same page) then set the target to self + if (anchors[x].hash && anchors[x].hostname == window.location.hostname) { + anchors[x].target = '_self'; + } + } + + // Hide the editor and display the previewer + if (!self.is('fullscreen')) { + self.editorIframe.style.display = 'none'; + self.previewerIframe.style.display = 'block'; + self._eeState.preview = true; + self._eeState.edit = false; + self.previewerIframe.focus(); + } + + self.emit('preview'); + return self; + } + + /** + * Puts the editor into fullscreen mode + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.enterFullscreen = function () { + if (this.is('fullscreen')) { return this; } + this._goFullscreen(this.iframeElement); + return this; + } + + /** + * Closes fullscreen mode if opened + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.exitFullscreen = function () { + if (!this.is('fullscreen')) { return this; } + this._exitFullscreen(this.iframeElement); + return this; + } + + /** + * Hides the preview and shows the editor again + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.edit = function () { + var self = this; + _replaceClass(self.getElement('wrapper'), 'epiceditor-preview-mode', 'epiceditor-edit-mode'); + self._eeState.preview = false; + self._eeState.edit = true; + self.editorIframe.style.display = 'block'; + self.previewerIframe.style.display = 'none'; + self.editorIframe.focus(); + self.emit('edit'); + return this; + } + + /** + * Grabs a specificed HTML node. Use it as a shortcut to getting the iframe contents + * @param {String} name The name of the node (can be document, body, editor, previewer, or wrapper) + * @returns {Object|Null} + */ + EpicEditor.prototype.getElement = function (name) { + var available = { + "container": this.element + , "wrapper": this.iframe.getElementById('epiceditor-wrapper') + , "wrapperIframe": this.iframeElement + , "editor": this.editorIframeDocument + , "editorIframe": this.editorIframe + , "previewer": this.previewerIframeDocument + , "previewerIframe": this.previewerIframe + } + + // Check that the given string is a possible option and verify the editor isn't unloaded + // without this, you'd be given a reference to an object that no longer exists in the DOM + if (!available[name] || this.is('unloaded')) { + return null; + } + else { + return available[name]; + } + } + + /** + * Returns a boolean of each "state" of the editor. For example "editor.is('loaded')" // returns true/false + * @param {String} what the state you want to check for + * @returns {Boolean} + */ + EpicEditor.prototype.is = function (what) { + var self = this; + switch (what) { + case 'loaded': + return self._eeState.loaded; + case 'unloaded': + return self._eeState.unloaded + case 'preview': + return self._eeState.preview + case 'edit': + return self._eeState.edit; + case 'fullscreen': + return self._eeState.fullscreen; + default: + return false; + } + } + + /** + * Opens a file + * @param {string} name The name of the file you want to open + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.open = function (name) { + var self = this + , defaultContent = self.settings.file.defaultContent + , fileObj; + name = name || self.settings.file.name; + self.settings.file.name = name; + if (this._storage[self.settings.localStorageName]) { + fileObj = self.getFiles(); + if (fileObj[name] !== undefined) { + _setText(self.editor, fileObj[name].content); + self.emit('read'); + } + else { + _setText(self.editor, defaultContent); + self.save(); // ensure a save + self.emit('create'); + } + self.previewer.innerHTML = self.exportFile(null, 'html'); + self.emit('open'); + } + return this; + } + + /** + * Saves content for offline use + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.save = function () { + var self = this + , storage + , isUpdate = false + , file = self.settings.file.name + , content = _getText(this.editor); + + // This could have been false but since we're manually saving + // we know it's save to start autoSaving again + this._canSave = true; + + storage = JSON.parse(this._storage[self.settings.localStorageName]); + + // If the file doesn't exist we need to create it + if (storage[file] === undefined) { + storage[file] = self._defaultFileSchema(); + } + + // If it does, we need to check if the content is different and + // if it is, send the update event and update the timestamp + else if (content !== storage[file].content) { + storage[file].modified = new Date(); + isUpdate = true; + } + + storage[file].content = content; + this._storage[self.settings.localStorageName] = JSON.stringify(storage); + + // After the content is actually changed, emit update so it emits the updated content + if (isUpdate) { + self.emit('update'); + } + + this.emit('save'); + return this; + } + + /** + * Removes a page + * @param {string} name The name of the file you want to remove from localStorage + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.remove = function (name) { + var self = this + , s; + name = name || self.settings.file.name; + + // If you're trying to delete a page you have open, block saving + if (name == self.settings.file.name) { + self._canSave = false; + } + + s = JSON.parse(this._storage[self.settings.localStorageName]); + delete s[name]; + this._storage[self.settings.localStorageName] = JSON.stringify(s); + this.emit('remove'); + return this; + }; + + /** + * Renames a file + * @param {string} oldName The old file name + * @param {string} newName The new file name + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.rename = function (oldName, newName) { + var self = this + , s = JSON.parse(this._storage[self.settings.localStorageName]); + s[newName] = s[oldName]; + delete s[oldName]; + this._storage[self.settings.localStorageName] = JSON.stringify(s); + self.open(newName); + return this; + }; + + /** + * Imports a file and it's contents and opens it + * @param {string} name The name of the file you want to import (will overwrite existing files!) + * @param {string} content Content of the file you want to import + * @param {string} kind The kind of file you want to import (TBI) + * @param {object} meta Meta data you want to save with your file. + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.importFile = function (name, content, kind, meta) { + var self = this + , isNew = false; + + name = name || self.settings.file.name; + content = content || ''; + kind = kind || 'md'; + meta = meta || {}; + + if (JSON.parse(this._storage[self.settings.localStorageName])[name] === undefined) { + isNew = true; + } + + // Set our current file to the new file and update the content + self.settings.file.name = name; + _setText(self.editor, content); + + if (isNew) { + self.emit('create'); + } + + self.save(); + + if (self.is('fullscreen')) { + self.preview(); + } + + return this; + }; + + /** + * Exports a file as a string in a supported format + * @param {string} name Name of the file you want to export (case sensitive) + * @param {string} kind Kind of file you want the content in (currently supports html and text) + * @returns {string|undefined} The content of the file in the content given or undefined if it doesn't exist + */ + EpicEditor.prototype.exportFile = function (name, kind) { + var self = this + , file + , content; + + name = name || self.settings.file.name; + kind = kind || 'text'; + + file = self.getFiles(name); + + // If the file doesn't exist just return early with undefined + if (file === undefined) { + return; + } + + content = file.content; + + switch (kind) { + case 'html': + // Get this, 2 spaces in a content editable actually converts to: + // 0020 00a0, meaning, "space no-break space". So, manually convert + // no-break spaces to spaces again before handing to marked. + // Also, WebKit converts no-break to unicode equivalent and FF HTML. + content = content.replace(/\u00a0/g, ' ').replace(/ /g, ' '); + return self.settings.parser(content); + case 'text': + content = content.replace(/\u00a0/g, ' ').replace(/ /g, ' '); + return content; + default: + return content; + } + } + + EpicEditor.prototype.getFiles = function (name) { + var files = JSON.parse(this._storage[this.settings.localStorageName]); + if (name) { + return files[name]; + } + else { + return files; + } + } + + // EVENTS + // TODO: Support for namespacing events like "preview.foo" + /** + * Sets up an event handler for a specified event + * @param {string} ev The event name + * @param {function} handler The callback to run when the event fires + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.on = function (ev, handler) { + var self = this; + if (!this.events[ev]) { + this.events[ev] = []; + } + this.events[ev].push(handler); + return self; + }; + + /** + * This will emit or "trigger" an event specified + * @param {string} ev The event name + * @param {any} data Any data you want to pass into the callback + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.emit = function (ev, data) { + var self = this + , x; + + data = data || self.getFiles(self.settings.file.name); + + if (!this.events[ev]) { + return; + } + + function invokeHandler(handler) { + handler.call(self, data); + } + + for (x = 0; x < self.events[ev].length; x++) { + invokeHandler(self.events[ev][x]); + } + + return self; + }; + + /** + * Will remove any listeners added from EpicEditor.on() + * @param {string} ev The event name + * @param {function} handler Handler to remove + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.removeListener = function (ev, handler) { + var self = this; + if (!handler) { + this.events[ev] = []; + return self; + } + if (!this.events[ev]) { + return self; + } + // Otherwise a handler and event exist, so take care of it + this.events[ev].splice(this.events[ev].indexOf(handler), 1); + return self; + } + + EpicEditor.version = '0.2.0'; + + // Used to store information to be shared across editors + EpicEditor._data = {}; + + window.EpicEditor = EpicEditor; +})(window); + +/** + * marked - A markdown parser (https://github.com/chjj/marked) + * Copyright (c) 2011-2012, Christopher Jeffrey. (MIT Licensed) + */ + +;(function() { + +/** + * Block-Level Grammar + */ + +var block = { + newline: /^\n+/, + code: /^( {4}[^\n]+\n*)+/, + fences: noop, + hr: /^( *[-*_]){3,} *(?:\n+|$)/, + heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, + lheading: /^([^\n]+)\n *(=|-){3,} *\n*/, + blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/, + list: /^( *)(bull) [^\0]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, + html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/, + def: /^ *\[([^\]]+)\]: *([^\s]+)(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, + paragraph: /^([^\n]+\n?(?!body))+\n*/, + text: /^[^\n]+/ +}; + +block.bullet = /(?:[*+-]|\d+\.)/; +block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; +block.item = replace(block.item, 'gm') + (/bull/g, block.bullet) + (); + +block.list = replace(block.list) + (/bull/g, block.bullet) + ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/) + (); + +block.html = replace(block.html) + ('comment', //) + ('closed', /<(tag)[^\0]+?<\/\1>/) + ('closing', /])*?>/) + (/tag/g, tag()) + (); + +block.paragraph = (function() { + var paragraph = block.paragraph.source + , body = []; + + (function push(rule) { + rule = block[rule] ? block[rule].source : rule; + body.push(rule.replace(/(^|[^\[])\^/g, '$1')); + return push; + }) + ('hr') + ('heading') + ('lheading') + ('blockquote') + ('<' + tag()) + ('def'); + + return new + RegExp(paragraph.replace('body', body.join('|'))); +})(); + +block.normal = { + fences: block.fences, + paragraph: block.paragraph +}; + +block.gfm = { + fences: /^ *``` *(\w+)? *\n([^\0]+?)\s*``` *(?:\n+|$)/, + paragraph: /^/ +}; + +block.gfm.paragraph = replace(block.paragraph) + ('(?!', '(?!' + block.gfm.fences.source.replace(/(^|[^\[])\^/g, '$1') + '|') + (); + +/** + * Block Lexer + */ + +block.lexer = function(src) { + var tokens = []; + + tokens.links = {}; + + src = src + .replace(/\r\n|\r/g, '\n') + .replace(/\t/g, ' '); + + return block.token(src, tokens, true); +}; + +block.token = function(src, tokens, top) { + var src = src.replace(/^ +$/gm, '') + , next + , loose + , cap + , item + , space + , i + , l; + + while (src) { + // newline + if (cap = block.newline.exec(src)) { + src = src.substring(cap[0].length); + if (cap[0].length > 1) { + tokens.push({ + type: 'space' + }); + } + } + + // code + if (cap = block.code.exec(src)) { + src = src.substring(cap[0].length); + cap = cap[0].replace(/^ {4}/gm, ''); + tokens.push({ + type: 'code', + text: !options.pedantic + ? cap.replace(/\n+$/, '') + : cap + }); + continue; + } + + // fences (gfm) + if (cap = block.fences.exec(src)) { + src = src.substring(cap[0].length); + tokens.push({ + type: 'code', + lang: cap[1], + text: cap[2] + }); + continue; + } + + // heading + if (cap = block.heading.exec(src)) { + src = src.substring(cap[0].length); + tokens.push({ + type: 'heading', + depth: cap[1].length, + text: cap[2] + }); + continue; + } + + // lheading + if (cap = block.lheading.exec(src)) { + src = src.substring(cap[0].length); + tokens.push({ + type: 'heading', + depth: cap[2] === '=' ? 1 : 2, + text: cap[1] + }); + continue; + } + + // hr + if (cap = block.hr.exec(src)) { + src = src.substring(cap[0].length); + tokens.push({ + type: 'hr' + }); + continue; + } + + // blockquote + if (cap = block.blockquote.exec(src)) { + src = src.substring(cap[0].length); + + tokens.push({ + type: 'blockquote_start' + }); + + cap = cap[0].replace(/^ *> ?/gm, ''); + + // Pass `top` to keep the current + // "toplevel" state. This is exactly + // how markdown.pl works. + block.token(cap, tokens, top); + + tokens.push({ + type: 'blockquote_end' + }); + + continue; + } + + // list + if (cap = block.list.exec(src)) { + src = src.substring(cap[0].length); + + tokens.push({ + type: 'list_start', + ordered: isFinite(cap[2]) + }); + + // Get each top-level item. + cap = cap[0].match(block.item); + + next = false; + l = cap.length; + i = 0; + + for (; i < l; i++) { + item = cap[i]; + + // Remove the list item's bullet + // so it is seen as the next token. + space = item.length; + item = item.replace(/^ *([*+-]|\d+\.) +/, ''); + + // Outdent whatever the + // list item contains. Hacky. + if (~item.indexOf('\n ')) { + space -= item.length; + item = !options.pedantic + ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') + : item.replace(/^ {1,4}/gm, ''); + } + + // Determine whether item is loose or not. + // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ + // for discount behavior. + loose = next || /\n\n(?!\s*$)/.test(item); + if (i !== l - 1) { + next = item[item.length-1] === '\n'; + if (!loose) loose = next; + } + + tokens.push({ + type: loose + ? 'loose_item_start' + : 'list_item_start' + }); + + // Recurse. + block.token(item, tokens); + + tokens.push({ + type: 'list_item_end' + }); + } + + tokens.push({ + type: 'list_end' + }); + + continue; + } + + // html + if (cap = block.html.exec(src)) { + src = src.substring(cap[0].length); + tokens.push({ + type: 'html', + pre: cap[1] === 'pre', + text: cap[0] + }); + continue; + } + + // def + if (top && (cap = block.def.exec(src))) { + src = src.substring(cap[0].length); + tokens.links[cap[1].toLowerCase()] = { + href: cap[2], + title: cap[3] + }; + continue; + } + + // top-level paragraph + if (top && (cap = block.paragraph.exec(src))) { + src = src.substring(cap[0].length); + tokens.push({ + type: 'paragraph', + text: cap[0] + }); + continue; + } + + // text + if (cap = block.text.exec(src)) { + // Top-level should never reach here. + src = src.substring(cap[0].length); + tokens.push({ + type: 'text', + text: cap[0] + }); + continue; + } + } + + return tokens; +}; + +/** + * Inline Processing + */ + +var inline = { + escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, + autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, + url: noop, + tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, + link: /^!?\[(inside)\]\(href\)/, + reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, + nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, + strong: /^__([^\0]+?)__(?!_)|^\*\*([^\0]+?)\*\*(?!\*)/, + em: /^\b_((?:__|[^\0])+?)_\b|^\*((?:\*\*|[^\0])+?)\*(?!\*)/, + code: /^(`+)([^\0]*?[^`])\1(?!`)/, + br: /^ {2,}\n(?!\s*$)/, + text: /^[^\0]+?(?=[\\?(?:\s+['"]([^\0]*?)['"])?\s*/; + +inline.link = replace(inline.link) + ('inside', inline._linkInside) + ('href', inline._linkHref) + (); + +inline.reflink = replace(inline.reflink) + ('inside', inline._linkInside) + (); + +inline.normal = { + url: inline.url, + strong: inline.strong, + em: inline.em, + text: inline.text +}; + +inline.pedantic = { + strong: /^__(?=\S)([^\0]*?\S)__(?!_)|^\*\*(?=\S)([^\0]*?\S)\*\*(?!\*)/, + em: /^_(?=\S)([^\0]*?\S)_(?!_)|^\*(?=\S)([^\0]*?\S)\*(?!\*)/ +}; + +inline.gfm = { + url: /^(https?:\/\/[^\s]+[^.,:;"')\]\s])/, + text: /^[^\0]+?(?=[\\' + + text + + ''; + continue; + } + + // url (gfm) + if (cap = inline.url.exec(src)) { + src = src.substring(cap[0].length); + text = escape(cap[1]); + href = text; + out += '' + + text + + ''; + continue; + } + + // tag + if (cap = inline.tag.exec(src)) { + src = src.substring(cap[0].length); + out += options.sanitize + ? escape(cap[0]) + : cap[0]; + continue; + } + + // link + if (cap = inline.link.exec(src)) { + src = src.substring(cap[0].length); + out += outputLink(cap, { + href: cap[2], + title: cap[3] + }); + continue; + } + + // reflink, nolink + if ((cap = inline.reflink.exec(src)) + || (cap = inline.nolink.exec(src))) { + src = src.substring(cap[0].length); + link = (cap[2] || cap[1]).replace(/\s+/g, ' '); + link = links[link.toLowerCase()]; + if (!link || !link.href) { + out += cap[0][0]; + src = cap[0].substring(1) + src; + continue; + } + out += outputLink(cap, link); + continue; + } + + // strong + if (cap = inline.strong.exec(src)) { + src = src.substring(cap[0].length); + out += '' + + inline.lexer(cap[2] || cap[1]) + + ''; + continue; + } + + // em + if (cap = inline.em.exec(src)) { + src = src.substring(cap[0].length); + out += '' + + inline.lexer(cap[2] || cap[1]) + + ''; + continue; + } + + // code + if (cap = inline.code.exec(src)) { + src = src.substring(cap[0].length); + out += '' + + escape(cap[2], true) + + ''; + continue; + } + + // br + if (cap = inline.br.exec(src)) { + src = src.substring(cap[0].length); + out += '
'; + continue; + } + + // text + if (cap = inline.text.exec(src)) { + src = src.substring(cap[0].length); + out += escape(cap[0]); + continue; + } + } + + return out; +}; + +function outputLink(cap, link) { + if (cap[0][0] !== '!') { + return '' + + inline.lexer(cap[1]) + + ''; + } else { + return ''
+      + escape(cap[1])
+      + ''; + } +} + +/** + * Parsing + */ + +var tokens + , token; + +function next() { + return token = tokens.pop(); +} + +function tok() { + switch (token.type) { + case 'space': { + return ''; + } + case 'hr': { + return '
\n'; + } + case 'heading': { + return '' + + inline.lexer(token.text) + + '\n'; + } + case 'code': { + if (options.highlight) { + token.code = options.highlight(token.text, token.lang); + if (token.code != null && token.code !== token.text) { + token.escaped = true; + token.text = token.code; + } + } + + if (!token.escaped) { + token.text = escape(token.text, true); + } + + return '
'
+        + token.text
+        + '
\n'; + } + case 'blockquote_start': { + var body = ''; + + while (next().type !== 'blockquote_end') { + body += tok(); + } + + return '
\n' + + body + + '
\n'; + } + case 'list_start': { + var type = token.ordered ? 'ol' : 'ul' + , body = ''; + + while (next().type !== 'list_end') { + body += tok(); + } + + return '<' + + type + + '>\n' + + body + + '\n'; + } + case 'list_item_start': { + var body = ''; + + while (next().type !== 'list_item_end') { + body += token.type === 'text' + ? parseText() + : tok(); + } + + return '
  • ' + + body + + '
  • \n'; + } + case 'loose_item_start': { + var body = ''; + + while (next().type !== 'list_item_end') { + body += tok(); + } + + return '
  • ' + + body + + '
  • \n'; + } + case 'html': { + if (options.sanitize) { + return inline.lexer(token.text); + } + return !token.pre && !options.pedantic + ? inline.lexer(token.text) + : token.text; + } + case 'paragraph': { + return '

    ' + + inline.lexer(token.text) + + '

    \n'; + } + case 'text': { + return '

    ' + + parseText() + + '

    \n'; + } + } +} + +function parseText() { + var body = token.text + , top; + + while ((top = tokens[tokens.length-1]) + && top.type === 'text') { + body += '\n' + next().text; + } + + return inline.lexer(body); +} + +function parse(src) { + tokens = src.reverse(); + + var out = ''; + while (next()) { + out += tok(); + } + + tokens = null; + token = null; + + return out; +} + +/** + * Helpers + */ + +function escape(html, encode) { + return html + .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function mangle(text) { + var out = '' + , l = text.length + , i = 0 + , ch; + + for (; i < l; i++) { + ch = text.charCodeAt(i); + if (Math.random() > 0.5) { + ch = 'x' + ch.toString(16); + } + out += '&#' + ch + ';'; + } + + return out; +} + +function tag() { + var tag = '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code' + + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo' + + '|span|br|wbr|ins|del|img)\\b)\\w+'; + + return tag; +} + +function replace(regex, opt) { + regex = regex.source; + opt = opt || ''; + return function self(name, val) { + if (!name) return new RegExp(regex, opt); + regex = regex.replace(name, val.source || val); + return self; + }; +} + +function noop() {} +noop.exec = noop; + +/** + * Marked + */ + +function marked(src, opt) { + setOptions(opt); + return parse(block.lexer(src)); +} + +/** + * Options + */ + +var options + , defaults; + +function setOptions(opt) { + if (!opt) opt = defaults; + if (options === opt) return; + options = opt; + + if (options.gfm) { + block.fences = block.gfm.fences; + block.paragraph = block.gfm.paragraph; + inline.text = inline.gfm.text; + inline.url = inline.gfm.url; + } else { + block.fences = block.normal.fences; + block.paragraph = block.normal.paragraph; + inline.text = inline.normal.text; + inline.url = inline.normal.url; + } + + if (options.pedantic) { + inline.em = inline.pedantic.em; + inline.strong = inline.pedantic.strong; + } else { + inline.em = inline.normal.em; + inline.strong = inline.normal.strong; + } +} + +marked.options = +marked.setOptions = function(opt) { + defaults = opt; + setOptions(opt); + return marked; +}; + +marked.setOptions({ + gfm: true, + pedantic: false, + sanitize: false, + highlight: null +}); + +/** + * Expose + */ + +marked.parser = function(src, opt) { + setOptions(opt); + return parse(src); +}; + +marked.lexer = function(src, opt) { + setOptions(opt); + return block.lexer(src); +}; + +marked.parse = marked; + +if (typeof module !== 'undefined') { + module.exports = marked; +} else { + this.marked = marked; +} + +}).call(function() { + return this || (typeof window !== 'undefined' ? window : global); +}()); diff --git a/website/scriptfan/static/lib/epiceditor/js/epiceditor.min.js b/website/scriptfan/static/lib/epiceditor/js/epiceditor.min.js new file mode 100644 index 0000000..f2f68d5 --- /dev/null +++ b/website/scriptfan/static/lib/epiceditor/js/epiceditor.min.js @@ -0,0 +1,4 @@ +/** + * EpicEditor - An Embeddable JavaScript Markdown Editor (https://github.com/OscarGodson/EpicEditor) + * Copyright (c) 2011-2012, Oscar Godson. (MIT Licensed) + */(function(e,t){function n(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])}function r(e,t){for(var n in t)t.hasOwnProperty(n)&&(e.style[n]=t[n])}function i(t,n){var r=t,i=null;return e.getComputedStyle?i=document.defaultView.getComputedStyle(r,null).getPropertyValue(n):r.currentStyle&&(i=r.currentStyle[n]),i}function s(e,t,n){var s={},o;if(t==="save"){for(o in n)n.hasOwnProperty(o)&&(s[o]=i(e,o));r(e,n)}else t==="apply"&&r(e,n);return s}function o(e){var t=parseInt(i(e,"border-left-width"),10)+parseInt(i(e,"border-right-width"),10),n=parseInt(i(e,"padding-left"),10)+parseInt(i(e,"padding-right"),10),r=e.offsetWidth,s;return isNaN(t)&&(t=0),s=t+n+r,s}function u(e){var t=parseInt(i(e,"border-top-width"),10)+parseInt(i(e,"border-bottom-width"),10),n=parseInt(i(e,"padding-top"),10)+parseInt(i(e,"padding-bottom"),10),r=e.offsetHeight,s;return isNaN(t)&&(t=0),s=t+n+r,s}function a(e,t,r){r=r||"";var i=t.getElementsByTagName("head")[0],s=t.createElement("link");n(s,{type:"text/css",id:r,rel:"stylesheet",href:e,name:e,media:"screen"}),i.appendChild(s)}function f(e,t,n){e.className=e.className.replace(t,n)}function l(e){return e.contentDocument||e.contentWindow.document}function c(e){var t;return typeof document.body.innerText=="string"?t=e.innerText:(t=e.innerHTML.replace(/
    /gi,"\n"),t=t.replace(/<(?:.|\n)*?>/gm,""),t=t.replace(/</gi,"<"),t=t.replace(/>/gi,">")),t}function h(e,t){return typeof document.body.innerText=="string"?(t=t.replace(/ /g," "),e.innerText=t):(t=t.replace(//g,">"),t=t.replace(/\n/g,"
    "),t=t.replace(/\s\s/g,"  "),e.innerHTML=t),!0}function p(){var e=-1,t=navigator.userAgent,n;return navigator.appName=="Microsoft Internet Explorer"&&(n=/MSIE ([0-9]{1,}[\.0-9]{0,})/,n.exec(t)!=null&&(e=parseFloat(RegExp.$1,10))),e}function d(){var t=e.navigator;return t.userAgent.indexOf("Safari")>-1&&t.userAgent.indexOf("Chrome")==-1}function v(e){var t={};return e&&t.toString.call(e)==="[object Function]"}function m(){var e=arguments[0]||{},n=1,r=arguments.length,i=!1,s,o,u,a;typeof e=="boolean"&&(i=e,e=arguments[1]||{},n=2),typeof e!="object"&&!v(e)&&(e={}),r===n&&(e=this,--n);for(;n=5||Math.abs(g.x-t.pageX)>=5)h.style.display="block",v&&clearTimeout(v),v=e.setTimeout(function(){h.style.display="none"},1e3);g={y:t.pageY,x:t.pageX}}function k(e){e.keyCode==n.settings.shortcut.modifier&&(S=!0),e.keyCode==17&&(x=!0),S===!0&&e.keyCode==n.settings.shortcut.preview&&!n.is("fullscreen")&&(e.preventDefault(),n.is("edit")?n.preview():n.edit()),S===!0&&e.keyCode==n.settings.shortcut.fullscreen&&(e.preventDefault(),n._goFullscreen(E)),S===!0&&e.keyCode!==n.settings.shortcut.modifier&&(S=!1),e.keyCode==27&&n.is("fullscreen")&&n._exitFullscreen(E),x===!0&&e.keyCode==83&&(n.save(),e.preventDefault(),x=!1),e.metaKey&&e.keyCode==83&&(n.save(),e.preventDefault())}function L(e){e.keyCode==n.settings.shortcut.modifier&&(S=!1),e.keyCode==17&&(x=!1)}if(this.is("loaded"))return this;var n=this,o,u,f,c,h,v,m,g={y:-1,x:-1},y,b,w=!1,E,S=!1,x=!1,T,N;n.settings.useNativeFullscreen&&(w=document.body.webkitRequestFullScreen?!0:!1),d()&&(w=!1),!n.is("edit")&&!n.is("preview")&&(n._eeState.edit=!0),t=t||function(){},o={chrome:'
    '+' '+''+"
    "+"
    ",previewer:'
    '},n.element.innerHTML='',n.element.style.height=n.element.offsetHeight+"px",u=document.getElementById(n._instanceId),n.iframeElement=u,n.iframe=l(u),n.iframe.open(),n.iframe.write(o.chrome),n.editorIframe=n.iframe.getElementById("epiceditor-editor-frame"),n.previewerIframe=n.iframe.getElementById("epiceditor-previewer-frame"),n.editorIframeDocument=l(n.editorIframe),n.editorIframeDocument.open(),n.editorIframeDocument.write(""),n.editorIframeDocument.close(),n.previewerIframeDocument=l(n.previewerIframe),n.previewerIframeDocument.open(),n.previewerIframeDocument.write(o.previewer),f=n.previewerIframeDocument.createElement("base"),f.target="_blank",n.previewerIframeDocument.getElementsByTagName("head")[0].appendChild(f),n.previewerIframeDocument.close(),n.reflow(),a(n.settings.basePath+n.settings.theme.base,n.iframe,"theme"),a(n.settings.basePath+n.settings.theme.editor,n.editorIframeDocument,"theme"),a(n.settings.basePath+n.settings.theme.preview,n.previewerIframeDocument,"theme"),n.iframe.getElementById("epiceditor-wrapper").style.position="relative",n.editor=n.editorIframeDocument.body,n.previewer=n.previewerIframeDocument.getElementById("epiceditor-preview"),n.editor.contentEditable=!0,n.iframe.body.style.height=this.element.offsetHeight+"px",this.previewerIframe.style.display="none",p()>-1&&(this.previewer.style.height=parseInt(i(this.previewer,"height"),10)+2),this.open(n.settings.file.name),n.settings.focusOnLoad&&n.iframe.addEventListener("readystatechange",function(){n.iframe.readyState=="complete"&&n.editorIframeDocument.body.focus()}),c=n.iframe.getElementById("epiceditor-utilbar"),y={},n._goFullscreen=function(t){if(n.is("fullscreen")){n._exitFullscreen(t);return}w&&t.webkitRequestFullScreen(),b=n.is("edit"),n._eeState.fullscreen=!0,n._eeState.edit=!0,n._eeState.preview=!0;var r=e.innerWidth,o=e.innerHeight,u=e.outerWidth,a=e.outerHeight;w||(a=e.innerHeight),y.editorIframe=s(n.editorIframe,"save",{width:u/2+"px",height:a+"px","float":"left",cssFloat:"left",styleFloat:"left",display:"block"}),y.previewerIframe=s(n.previewerIframe,"save",{width:u/2+"px",height:a+"px","float":"right",cssFloat:"right",styleFloat:"right",display:"block"}),y.element=s(n.element,"save",{position:"fixed",top:"0",left:"0",width:"100%","z-index":"9999",zIndex:"9999",border:"none",margin:"0",background:i(n.editor,"background-color"),height:o+"px"}),y.iframeElement=s(n.iframeElement,"save",{width:u+"px",height:o+"px"}),c.style.visibility="hidden",w||(document.body.style.overflow="hidden"),n.preview(),n.editorIframeDocument.body.focus(),n.emit("fullscreenenter")},n._exitFullscreen=function(e){s(n.element,"apply",y.element),s(n.iframeElement,"apply",y.iframeElement),s(n.editorIframe,"apply",y.editorIframe),s(n.previewerIframe,"apply",y.previewerIframe),n.element.style.width=n._eeState.reflowWidth?n._eeState.reflowWidth:"",n.element.style.height=n._eeState.reflowHeight?n._eeState.reflowHeight:"",c.style.visibility="visible",w?document.webkitCancelFullScreen():document.body.style.overflow="auto",n._eeState.fullscreen=!1,b?n.edit():n.preview(),n.reflow(),n.emit("fullscreenexit")},n.editor.addEventListener("keyup",function(){m&&e.clearTimeout(m),m=e.setTimeout(function(){n.is("fullscreen")&&n.preview()},250)}),E=n.iframeElement,c.addEventListener("click",function(e){var t=e.target.className;t.indexOf("epiceditor-toggle-preview-btn")>-1?n.preview():t.indexOf("epiceditor-toggle-edit-btn")>-1?n.edit():t.indexOf("epiceditor-fullscreen-btn")>-1&&n._goFullscreen(E)}),document.body.webkitRequestFullScreen&&E.addEventListener("webkitfullscreenchange",function(){document.webkitIsFullScreen||n._exitFullscreen(E)},!1),h=n.iframe.getElementById("epiceditor-utilbar"),h.style.display="none",h.addEventListener("mouseover",function(){v&&clearTimeout(v)}),T=[n.previewerIframeDocument,n.editorIframeDocument];for(N=0;N"+t.lexer(e[1])+"":''+f(e[1])+'"}function s(){return i=r.pop()}function o(){switch(i.type){case"space":return"";case"hr":return"
    \n";case"heading":return""+t.lexer(i.text)+"\n";case"code":return v.highlight&&(i.code=v.highlight(i.text,i.lang),i.code!=null&&i.code!==i.text&&(i.escaped=!0,i.text=i.code)),i.escaped||(i.text=f(i.text,!0)),"
    "+i.text+"
    \n";case"blockquote_start":var e="";while(s().type!=="blockquote_end")e+=o();return"
    \n"+e+"
    \n";case"list_start":var n=i.ordered?"ol":"ul",e="";while(s().type!=="list_end")e+=o();return"<"+n+">\n"+e+"\n";case"list_item_start":var e="";while(s().type!=="list_item_end")e+=i.type==="text"?u():o();return"
  • "+e+"
  • \n";case"loose_item_start":var e="";while(s().type!=="list_item_end")e+=o();return"
  • "+e+"
  • \n";case"html":return v.sanitize?t.lexer(i.text):!i.pre&&!v.pedantic?t.lexer(i.text):i.text;case"paragraph":return"

    "+t.lexer(i.text)+"

    \n";case"text":return"

    "+u()+"

    \n"}}function u(){var e=i.text,n;while((n=r[r.length-1])&&n.type==="text")e+="\n"+s().text;return t.lexer(e)}function a(e){r=e.reverse();var t="";while(s())t+=o();return r=null,i=null,t}function f(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function l(e){var t="",n=e.length,r=0,i;for(;r.5&&(i="x"+i.toString(16)),t+="&#"+i+";";return t}function c(){var e="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+";return e}function h(e,t){return e=e.source,t=t||"",function n(r,i){return r?(e=e.replace(r,i.source||i),n):new RegExp(e,t)}}function p(){}function d(t,n){return g(n),a(e.lexer(t))}function g(n){n||(n=m);if(v===n)return;v=n,v.gfm?(e.fences=e.gfm.fences,e.paragraph=e.gfm.paragraph,t.text=t.gfm.text,t.url=t.gfm.url):(e.fences=e.normal.fences,e.paragraph=e.normal.paragraph,t.text=t.normal.text,t.url=t.normal.url),v.pedantic?(t.em=t.pedantic.em,t.strong=t.pedantic.strong):(t.em=t.normal.em,t.strong=t.normal.strong)}var e={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:p,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,lheading:/^([^\n]+)\n *(=|-){3,} *\n*/,blockquote:/^( *>[^\n]+(\n[^\n]+)*\n*)+/,list:/^( *)(bull) [^\0]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/,def:/^ *\[([^\]]+)\]: *([^\s]+)(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,paragraph:/^([^\n]+\n?(?!body))+\n*/,text:/^[^\n]+/};e.bullet=/(?:[*+-]|\d+\.)/,e.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,e.item=h(e.item,"gm")(/bull/g,e.bullet)(),e.list=h(e.list)(/bull/g,e.bullet)("hr",/\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/)(),e.html=h(e.html)("comment",//)("closed",/<(tag)[^\0]+?<\/\1>/)("closing",/])*?>/)(/tag/g,c())(),e.paragraph=function(){var t=e.paragraph.source,n=[];return function r(t){return t=e[t]?e[t].source:t,n.push(t.replace(/(^|[^\[])\^/g,"$1")),r}("hr")("heading")("lheading")("blockquote")("<"+c())("def"),new RegExp(t.replace("body",n.join("|")))}(),e.normal={fences:e.fences,paragraph:e.paragraph},e.gfm={fences:/^ *``` *(\w+)? *\n([^\0]+?)\s*``` *(?:\n+|$)/,paragraph:/^/},e.gfm.paragraph=h(e.paragraph)("(?!","(?!"+e.gfm.fences.source.replace(/(^|[^\[])\^/g,"$1")+"|")(),e.lexer=function(t){var n=[];return n.links={},t=t.replace(/\r\n|\r/g,"\n").replace(/\t/g," "),e.token(t,n,!0)},e.token=function(t,n,r){var t=t.replace(/^ +$/gm,""),i,s,o,u,a,f,l;while(t){if(o=e.newline.exec(t))t=t.substring(o[0].length),o[0].length>1&&n.push({type:"space"});if(o=e.code.exec(t)){t=t.substring(o[0].length),o=o[0].replace(/^ {4}/gm,""),n.push({type:"code",text:v.pedantic?o:o.replace(/\n+$/,"")});continue}if(o=e.fences.exec(t)){t=t.substring(o[0].length),n.push({type:"code",lang:o[1],text:o[2]});continue}if(o=e.heading.exec(t)){t=t.substring(o[0].length),n.push({type:"heading",depth:o[1].length,text:o[2]});continue}if(o=e.lheading.exec(t)){t=t.substring(o[0].length),n.push({type:"heading",depth:o[2]==="="?1:2,text:o[1]});continue}if(o=e.hr.exec(t)){t=t.substring(o[0].length),n.push({type:"hr"});continue}if(o=e.blockquote.exec(t)){t=t.substring(o[0].length),n.push({type:"blockquote_start"}),o=o[0].replace(/^ *> ?/gm,""),e.token(o,n,r),n.push({type:"blockquote_end"});continue}if(o=e.list.exec(t)){t=t.substring(o[0].length),n.push({type:"list_start",ordered:isFinite(o[2])}),o=o[0].match(e.item),i=!1,l=o.length,f=0;for(;f])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:p,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([^\0]+?)__(?!_)|^\*\*([^\0]+?)\*\*(?!\*)/,em:/^\b_((?:__|[^\0])+?)_\b|^\*((?:\*\*|[^\0])+?)\*(?!\*)/,code:/^(`+)([^\0]*?[^`])\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,text:/^[^\0]+?(?=[\\?(?:\s+['"]([^\0]*?)['"])?\s*/,t.link=h(t.link)("inside",t._linkInside)("href",t._linkHref)(),t.reflink=h(t.reflink)("inside",t._linkInside)(),t.normal={url:t.url,strong:t.strong,em:t.em,text:t.text},t.pedantic={strong:/^__(?=\S)([^\0]*?\S)__(?!_)|^\*\*(?=\S)([^\0]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([^\0]*?\S)_(?!_)|^\*(?=\S)([^\0]*?\S)\*(?!\*)/},t.gfm={url:/^(https?:\/\/[^\s]+[^.,:;"')\]\s])/,text:/^[^\0]+?(?=[\\'+u+"";continue}if(c=t.url.exec(e)){e=e.substring(c[0].length),u=f(c[1]),a=u,i+=''+u+"";continue}if(c=t.tag.exec(e)){e=e.substring(c[0].length),i+=v.sanitize?f(c[0]):c[0];continue}if(c=t.link.exec(e)){e=e.substring(c[0].length),i+=n(c,{href:c[2],title:c[3]});continue}if((c=t.reflink.exec(e))||(c=t.nolink.exec(e))){e=e.substring(c[0].length),o=(c[2]||c[1]).replace(/\s+/g," "),o=s[o.toLowerCase()];if(!o||!o.href){i+=c[0][0],e=c[0].substring(1)+e;continue}i+=n(c,o);continue}if(c=t.strong.exec(e)){e=e.substring(c[0].length),i+=""+t.lexer(c[2]||c[1])+"";continue}if(c=t.em.exec(e)){e=e.substring(c[0].length),i+=""+t.lexer(c[2]||c[1])+"";continue}if(c=t.code.exec(e)){e=e.substring(c[0].length),i+=""+f(c[2],!0)+"";continue}if(c=t.br.exec(e)){e=e.substring(c[0].length),i+="
    ";continue}if(c=t.text.exec(e)){e=e.substring(c[0].length),i+=f(c[0]);continue}}return i};var r,i;p.exec=p;var v,m;d.options=d.setOptions=function(e){return m=e,g(e),d},d.setOptions({gfm:!0,pedantic:!1,sanitize:!1,highlight:null}),d.parser=function(e,t){return g(t),a(e)},d.lexer=function(t,n){return g(n),e.lexer(t)},d.parse=d,typeof module!="undefined"?module.exports=d:this.marked=d}.call(function(){return this||(typeof window!="undefined"?window:global)}()); \ No newline at end of file diff --git a/website/scriptfan/static/lib/epiceditor/themes/base/epiceditor.css b/website/scriptfan/static/lib/epiceditor/themes/base/epiceditor.css new file mode 100644 index 0000000..35ad611 --- /dev/null +++ b/website/scriptfan/static/lib/epiceditor/themes/base/epiceditor.css @@ -0,0 +1,31 @@ +html, body, iframe, div { margin:0; padding:0; } + +#epiceditor-utilbar { + position:fixed; + bottom:10px; + right:10px; + padding:5px; +} + +#epiceditor-utilbar img { + display:block; + float:left; + width: 30px; + height: 30px; +} + +#epiceditor-utilbar img:last-child { + margin-left: 15px; +} + +#epiceditor-utilbar img:hover { + cursor:pointer; +} + +.epiceditor-edit-mode #epiceditor-utilbar img.epiceditor-toggle-edit-btn { + display: none; +} + +.epiceditor-preview-mode #epiceditor-utilbar img.epiceditor-toggle-preview-btn { + display: none; +} diff --git a/website/scriptfan/static/lib/epiceditor/themes/editor/epic-dark.css b/website/scriptfan/static/lib/epiceditor/themes/editor/epic-dark.css new file mode 100644 index 0000000..058ace6 --- /dev/null +++ b/website/scriptfan/static/lib/epiceditor/themes/editor/epic-dark.css @@ -0,0 +1,13 @@ +html { padding:10px; } + +body { + border:0; + background:rgb(41,41,41); + font-family:monospace; + font-size:14px; + padding:10px; + color:#ddd; + line-height:1.35em; + margin:0; + padding:0; +} diff --git a/website/scriptfan/static/lib/epiceditor/themes/editor/epic-light.css b/website/scriptfan/static/lib/epiceditor/themes/editor/epic-light.css new file mode 100644 index 0000000..9411cec --- /dev/null +++ b/website/scriptfan/static/lib/epiceditor/themes/editor/epic-light.css @@ -0,0 +1,12 @@ +html { padding:10px; } + +body { + border:0; + background:#fcfcfc; + font-family:monospace; + font-size:14px; + padding:10px; + line-height:1.35em; + margin:0; + padding:0; +} diff --git a/website/scriptfan/static/lib/epiceditor/themes/preview/bartik.css b/website/scriptfan/static/lib/epiceditor/themes/preview/bartik.css new file mode 100644 index 0000000..2ffb6d5 --- /dev/null +++ b/website/scriptfan/static/lib/epiceditor/themes/preview/bartik.css @@ -0,0 +1,167 @@ +body { + font-family: Georgia, "Times New Roman", Times, serif; + line-height: 1.5; + font-size: 87.5%; + word-wrap: break-word; + margin: 2em; + padding: 0; + border: 0; + outline: 0; + background: #fff; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1.0em 0 0.5em; + font-weight: inherit; +} + +h1 { + font-size: 1.357em; + color: #000; +} + +h2 { + font-size: 1.143em; +} + +p { + margin: 0 0 1.2em; +} + +del { + text-decoration: line-through; +} + +tr:nth-child(odd) { + background-color: #dddddd; +} + +img { + outline: 0; +} + +code { + background-color: #f2f2f2; + background-color: rgba(40, 40, 0, 0.06); +} + +pre { + background-color: #f2f2f2; + background-color: rgba(40, 40, 0, 0.06); + margin: 10px 0; + overflow: hidden; + padding: 15px; + white-space: pre-wrap; +} + +pre code { + font-size: 100%; + background-color: transparent; +} + +blockquote { + background: #f7f7f7; + border-left: 1px solid #bbb; + font-style: italic; + margin: 1.5em 10px; + padding: 0.5em 10px; +} + +blockquote:before { + color: #bbb; + content: "\201C"; + font-size: 3em; + line-height: 0.1em; + margin-right: 0.2em; + vertical-align: -.4em; +} + +blockquote:after { + color: #bbb; + content: "\201D"; + font-size: 3em; + line-height: 0.1em; + vertical-align: -.45em; +} + +blockquote > p:first-child { + display: inline; +} + +table { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + border: 0; + border-spacing: 0; + font-size: 0.857em; + margin: 10px 0; + width: 100%; +} + +table table { + font-size: 1em; +} + +table tr th { + background: #757575; + background: rgba(0, 0, 0, 0.51); + border-bottom-style: none; +} + +table tr th, +table tr th a, +table tr th a:hover { + color: #FFF; + font-weight: bold; +} + +table tbody tr th { + vertical-align: top; +} + +tr td, +tr th { + padding: 4px 9px; + border: 1px solid #fff; + text-align: left; /* LTR */ +} + +tr:nth-child(odd) { + background: #e4e4e4; + background: rgba(0, 0, 0, 0.105); +} + +tr, +tr:nth-child(even) { + background: #efefef; + background: rgba(0, 0, 0, 0.063); +} + +a { + color: #0071B3; +} + +a:hover, +a:focus { + color: #018fe2; +} + +a:active { + color: #23aeff; +} + +a:link, +a:visited { + text-decoration: none; +} + +a:hover, +a:active, +a:focus { + text-decoration: underline; +} + diff --git a/website/scriptfan/static/lib/epiceditor/themes/preview/github.css b/website/scriptfan/static/lib/epiceditor/themes/preview/github.css new file mode 100644 index 0000000..4c78db4 --- /dev/null +++ b/website/scriptfan/static/lib/epiceditor/themes/preview/github.css @@ -0,0 +1,368 @@ +html { padding:0 10px; } + +body { + margin:0; + padding:0; + background:#fff; +} + +#epiceditor-wrapper{ + background:white; +} + +#epiceditor-preview{ + padding-top:10px; + padding-bottom:10px; + font-family: Helvetica,arial,freesans,clean,sans-serif; + font-size:13px; + line-height:1.6; +} + +#epiceditor-preview>*:first-child{ + margin-top:0!important; +} + +#epiceditor-preview>*:last-child{ + margin-bottom:0!important; +} + +#epiceditor-preview a{ + color:#4183C4; + text-decoration:none; +} + +#epiceditor-preview a:hover{ + text-decoration:underline; +} + +#epiceditor-preview h1, +#epiceditor-preview h2, +#epiceditor-preview h3, +#epiceditor-preview h4, +#epiceditor-preview h5, +#epiceditor-preview h6{ + margin:20px 0 10px; + padding:0; + font-weight:bold; + -webkit-font-smoothing:antialiased; +} + +#epiceditor-preview h1 tt, +#epiceditor-preview h1 code, +#epiceditor-preview h2 tt, +#epiceditor-preview h2 code, +#epiceditor-preview h3 tt, +#epiceditor-preview h3 code, +#epiceditor-preview h4 tt, +#epiceditor-preview h4 code, +#epiceditor-preview h5 tt, +#epiceditor-preview h5 code, +#epiceditor-preview h6 tt, +#epiceditor-preview h6 code{ + font-size:inherit; +} + +#epiceditor-preview h1{ + font-size:28px; + color:#000; +} + +#epiceditor-preview h2{ + font-size:24px; + border-bottom:1px solid #ccc; + color:#000; +} + +#epiceditor-preview h3{ + font-size:18px; +} + +#epiceditor-preview h4{ + font-size:16px; +} + +#epiceditor-preview h5{ + font-size:14px; +} + +#epiceditor-preview h6{ + color:#777; + font-size:14px; +} + +#epiceditor-preview p, +#epiceditor-preview blockquote, +#epiceditor-preview ul, +#epiceditor-preview ol, +#epiceditor-preview dl, +#epiceditor-preview li, +#epiceditor-preview table, +#epiceditor-preview pre{ + margin:15px 0; +} + +#epiceditor-preview hr{ + background:transparent url('../../images/modules/pulls/dirty-shade.png') repeat-x 0 0; + border:0 none; + color:#ccc; + height:4px; + padding:0; +} + +#epiceditor-preview>h2:first-child, +#epiceditor-preview>h1:first-child, +#epiceditor-preview>h1:first-child+h2, +#epiceditor-preview>h3:first-child, +#epiceditor-preview>h4:first-child, +#epiceditor-preview>h5:first-child, +#epiceditor-preview>h6:first-child{ + margin-top:0; + padding-top:0; +} + +#epiceditor-preview h1+p, +#epiceditor-preview h2+p, +#epiceditor-preview h3+p, +#epiceditor-preview h4+p, +#epiceditor-preview h5+p, +#epiceditor-preview h6+p{ + margin-top:0; +} + +#epiceditor-preview li p.first{ + display:inline-block; +} + +#epiceditor-preview ul, +#epiceditor-preview ol{ + padding-left:30px; +} + +#epiceditor-preview ul li>:first-child, +#epiceditor-preview ol li>:first-child{ + margin-top:0; +} + +#epiceditor-preview ul li>:last-child, +#epiceditor-preview ol li>:last-child{ + margin-bottom:0; +} + +#epiceditor-preview dl{ + padding:0; +} + +#epiceditor-preview dl dt{ + font-size:14px; + font-weight:bold; + font-style:italic; + padding:0; + margin:15px 0 5px; +} + +#epiceditor-preview dl dt:first-child{ + padding:0; +} + +#epiceditor-preview dl dt>:first-child{ + margin-top:0; +} + +#epiceditor-preview dl dt>:last-child{ + margin-bottom:0; +} + +#epiceditor-preview dl dd{ + margin:0 0 15px; + padding:0 15px; +} + +#epiceditor-preview dl dd>:first-child{ + margin-top:0; +} + +#epiceditor-preview dl dd>:last-child{ + margin-bottom:0; +} + +#epiceditor-preview blockquote{ + border-left:4px solid #DDD; + padding:0 15px; + color:#777; +} + +#epiceditor-preview blockquote>:first-child{ + margin-top:0; +} + +#epiceditor-preview blockquote>:last-child{ + margin-bottom:0; +} + +#epiceditor-preview table{ + padding:0; + border-collapse: collapse; + border-spacing: 0; + font-size: 100%; + font: inherit; +} + +#epiceditor-preview table tr{ + border-top:1px solid #ccc; + background-color:#fff; + margin:0; + padding:0; +} + +#epiceditor-preview table tr:nth-child(2n){ + background-color:#f8f8f8; +} + +#epiceditor-preview table tr th{ + font-weight:bold; +} + +#epiceditor-preview table tr th, +#epiceditor-preview table tr td{ + border:1px solid #ccc; + text-align:left; + margin:0; + padding:6px 13px; +} + +#epiceditor-preview table tr th>:first-child, +#epiceditor-preview table tr td>:first-child{ + margin-top:0; +} + +#epiceditor-preview table tr th>:last-child, +#epiceditor-preview table tr td>:last-child{ + margin-bottom:0; +} + +#epiceditor-preview img{ + max-width:100%; +} + +#epiceditor-preview span.frame{ + display:block; + overflow:hidden; +} + +#epiceditor-preview span.frame>span{ + border:1px solid #ddd; + display:block; + float:left; + overflow:hidden; + margin:13px 0 0; + padding:7px; + width:auto; +} + +#epiceditor-preview span.frame span img{ + display:block; + float:left; +} + +#epiceditor-preview span.frame span span{ + clear:both; + color:#333; + display:block; + padding:5px 0 0; +} + +#epiceditor-preview span.align-center{ + display:block; + overflow:hidden; + clear:both; +} + +#epiceditor-preview span.align-center>span{ + display:block; + overflow:hidden; + margin:13px auto 0; + text-align:center; +} + +#epiceditor-preview span.align-center span img{ + margin:0 auto; + text-align:center; +} + +#epiceditor-preview span.align-right{ + display:block; + overflow:hidden; + clear:both; +} + +#epiceditor-preview span.align-right>span{ + display:block; + overflow:hidden; + margin:13px 0 0; + text-align:right; +} + +#epiceditor-preview span.align-right span img{ + margin:0; + text-align:right; +} + +#epiceditor-preview span.float-left{ + display:block; + margin-right:13px; + overflow:hidden; + float:left; +} + +#epiceditor-preview span.float-left span{ + margin:13px 0 0; +} + +#epiceditor-preview span.float-right{ + display:block; + margin-left:13px; + overflow:hidden; + float:right; +} + +#epiceditor-preview span.float-right>span{ + display:block; + overflow:hidden; + margin:13px auto 0; + text-align:right; +} + +#epiceditor-preview code, +#epiceditor-preview tt{ + margin:0 2px; + padding:0 5px; + white-space:nowrap; + border:1px solid #eaeaea; + background-color:#f8f8f8; + border-radius:3px; +} + +#epiceditor-preview pre>code{ + margin:0; + padding:0; + white-space:pre; + border:none; + background:transparent; +} + +#epiceditor-preview .highlight pre, +#epiceditor-preview pre{ + background-color:#f8f8f8; + border:1px solid #ccc; + font-size:13px; + line-height:19px; + overflow:auto; + padding:6px 10px; + border-radius:3px; +} + +#epiceditor-preview pre code, +#epiceditor-preview pre tt{ + background-color:transparent; + border:none; +} diff --git a/website/scriptfan/static/lib/epiceditor/themes/preview/preview-dark.css b/website/scriptfan/static/lib/epiceditor/themes/preview/preview-dark.css new file mode 100644 index 0000000..620c193 --- /dev/null +++ b/website/scriptfan/static/lib/epiceditor/themes/preview/preview-dark.css @@ -0,0 +1,121 @@ +html { padding:0 10px; } + +body { + margin:0; + padding:10px 0; + background:#000; +} + +#epiceditor-preview h1, +#epiceditor-preview h2, +#epiceditor-preview h3, +#epiceditor-preview h4, +#epiceditor-preview h5, +#epiceditor-preview h6, +#epiceditor-preview p, +#epiceditor-preview blockquote { + margin: 0; + padding: 0; +} +#epiceditor-preview { + background:#000; + font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif; + font-size: 13px; + line-height: 18px; + color: #ccc; +} +#epiceditor-preview a { + color: #fff; +} +#epiceditor-preview a:hover { + color: #00ff00; + text-decoration: none; +} +#epiceditor-preview a img { + border: none; +} +#epiceditor-preview p { + margin-bottom: 9px; +} +#epiceditor-preview h1, +#epiceditor-preview h2, +#epiceditor-preview h3, +#epiceditor-preview h4, +#epiceditor-preview h5, +#epiceditor-preview h6 { + color: #cdcdcd; + line-height: 36px; +} +#epiceditor-preview h1 { + margin-bottom: 18px; + font-size: 30px; +} +#epiceditor-preview h2 { + font-size: 24px; +} +#epiceditor-preview h3 { + font-size: 18px; +} +#epiceditor-preview h4 { + font-size: 16px; +} +#epiceditor-preview h5 { + font-size: 14px; +} +#epiceditor-preview h6 { + font-size: 13px; +} +#epiceditor-preview hr { + margin: 0 0 19px; + border: 0; + border-bottom: 1px solid #ccc; +} +#epiceditor-preview blockquote { + padding: 13px 13px 21px 15px; + margin-bottom: 18px; + font-family:georgia,serif; + font-style: italic; +} +#epiceditor-preview blockquote:before { + content:"\201C"; + font-size:40px; + margin-left:-10px; + font-family:georgia,serif; + color:#eee; +} +#epiceditor-preview blockquote p { + font-size: 14px; + font-weight: 300; + line-height: 18px; + margin-bottom: 0; + font-style: italic; +} +#epiceditor-preview code, #epiceditor-preview pre { + font-family: Monaco, Andale Mono, Courier New, monospace; +} +#epiceditor-preview code { + background-color: #000; + color: #f92672; + padding: 1px 3px; + font-size: 12px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +#epiceditor-preview pre { + display: block; + padding: 14px; + color:#66d9ef; + margin: 0 0 18px; + line-height: 16px; + font-size: 11px; + border: 1px solid #d9d9d9; + white-space: pre-wrap; + word-wrap: break-word; +} +#epiceditor-preview pre code { + background-color: #000; + color:#ccc; + font-size: 11px; + padding: 0; +} diff --git a/website/scriptfan/templates/articles/_form.html b/website/scriptfan/templates/articles/_form.html new file mode 100644 index 0000000..f16f05a --- /dev/null +++ b/website/scriptfan/templates/articles/_form.html @@ -0,0 +1,19 @@ +
    + {{ form.hidden_tag() }} +
    +
    + {{ form.title(placeholder=_('models.article.title'), class='input-xxlarge') }} + {{ form.title | error_text }} +
    +
    +
    +
    + {{ form.content(style='display: none;') }} +
    + {{ form.content | error_text }} +
    +
    +
    + +
    +
    diff --git a/website/scriptfan/templates/articles/edit.html b/website/scriptfan/templates/articles/edit.html new file mode 100644 index 0000000..3c95a89 --- /dev/null +++ b/website/scriptfan/templates/articles/edit.html @@ -0,0 +1,23 @@ +{% extends "layout.html" %} + +{% block title %}{{ _('views.articles.edit.title') % form.title.data }}{% endblock %} + +{% block styles %} + +{% endblock %} + +{% block scripts %} + +{{ super() }} +{% endblock %} + +{% block content %} +
    + + + {% include 'articles/_form.html' %} +
    +{% endblock %} + diff --git a/website/scriptfan/templates/articles/form.html b/website/scriptfan/templates/articles/form.html deleted file mode 100644 index 1b966e4..0000000 --- a/website/scriptfan/templates/articles/form.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "layout.html" %} - -{% block title %}{{ title }}{% endblock %} - -{% block content %} -
    - - -
    - {{ form.hidden_tag() }} -
    - -
    - {{ form.title }} - {{ form.title | error_text }} -
    -
    -
    - -
    - {{ form.content }} - {{ form.content | error_text }} -
    -
    -
    - -
    -
    -
    -{% endblock %} - diff --git a/website/scriptfan/templates/articles/index.html b/website/scriptfan/templates/articles/index.html index bede814..58924ee 100644 --- a/website/scriptfan/templates/articles/index.html +++ b/website/scriptfan/templates/articles/index.html @@ -6,15 +6,15 @@
      {% for article in articles %}
    • -

      {{ article.title }}

      - +

      {{ article.title }}

      +
      {{ article.content | markdown | safe }} diff --git a/website/scriptfan/templates/articles/new.html b/website/scriptfan/templates/articles/new.html new file mode 100644 index 0000000..a73c5fe --- /dev/null +++ b/website/scriptfan/templates/articles/new.html @@ -0,0 +1,23 @@ +{% extends "layout.html" %} + +{% block title %}{{ _('views.articles.new.title') }}{% endblock %} + +{% block styles %} + +{% endblock %} + +{% block scripts %} + +{{ super() }} +{% endblock %} + +{% block content %} +
      + + + {% include 'articles/_form.html' %} +
      +{% endblock %} + diff --git a/website/scriptfan/templates/articles/show.html b/website/scriptfan/templates/articles/show.html new file mode 100644 index 0000000..42dedc5 --- /dev/null +++ b/website/scriptfan/templates/articles/show.html @@ -0,0 +1,24 @@ +{% extends "layout.html" %} + +{% block title %}{{ article.title }}{% endblock %} + +{% block content %} +
      + + +
      +
      + +
      +
      + {# TODO: Use article.content_html instead of content #} + {{ article.content | markdown | safe }} +
      +
      +
      +{% endblock %} diff --git a/website/scriptfan/templates/enviroment.html b/website/scriptfan/templates/enviroment.html new file mode 100644 index 0000000..3c0b8a4 --- /dev/null +++ b/website/scriptfan/templates/enviroment.html @@ -0,0 +1 @@ +fdsa diff --git a/website/scriptfan/templates/enviroment.js b/website/scriptfan/templates/enviroment.js new file mode 100644 index 0000000..769c823 --- /dev/null +++ b/website/scriptfan/templates/enviroment.js @@ -0,0 +1,6 @@ +ENV = { + ROOT: '{{ request.url_root }}', + STATIC: '{{ url_for('static', filename='') }}', + VERSION: '1.0' +}; + diff --git a/website/scriptfan/templates/layout.html b/website/scriptfan/templates/layout.html index b7d89de..367d090 100644 --- a/website/scriptfan/templates/layout.html +++ b/website/scriptfan/templates/layout.html @@ -8,16 +8,17 @@ {{ t.css('lib/bootstrap/css/bootstrap.min.css') }} {{ t.css('css/styles.css') }} - - {% block styles %}{% endblock %} + {% block styles %} + {{ t.css('css/%s.css' % request.blueprint) }} + {% endblock %}
      +
      diff --git a/website/scriptfan/templates/articles/index.html b/website/scriptfan/templates/articles/index.html index 69e3af6..618960d 100644 --- a/website/scriptfan/templates/articles/index.html +++ b/website/scriptfan/templates/articles/index.html @@ -16,6 +16,7 @@

      {{ _('articles') }}

      diff --git a/website/scriptfan/templates/articles/show.html b/website/scriptfan/templates/articles/show.html index 36ed307..20ee15e 100644 --- a/website/scriptfan/templates/articles/show.html +++ b/website/scriptfan/templates/articles/show.html @@ -13,6 +13,7 @@

      {{ article.title }}

      diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 13efd1b..93a5558 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -30,7 +30,7 @@ def index(): @blueprint.route('/', methods=['GET']) def show(article_id): - article = Article.get_by_id(article_id) + article = Article.query.get(article_id) return render_template('articles/show.html', article=article) @@ -53,7 +53,7 @@ def create(): @blueprint.route('/edit/', methods=['GET', 'POST']) @login.login_required def update(article_id): - article = Article.get_by_id(article_id) + article = Article.query.get(article_id) form = ArticleForm(obj=article) if form.validate_on_submit(): form.populate_obj(article) @@ -61,4 +61,5 @@ def update(article_id): flash('Update article successfully!', 'success') return redirect(url_for('.index')) + app.logger.info(form.data) return render_template('articles/edit.html', form=form) From 7145316df00a129f7bf69c100bd73774074b062b Mon Sep 17 00:00:00 2001 From: greatghoul Date: Tue, 23 Apr 2013 20:30:30 +0800 Subject: [PATCH 091/119] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=B0=86=E6=96=87?= =?UTF-8?q?=E7=AB=A0=E5=86=85=E5=AE=B9=E4=BD=9C=E4=B8=BAtag=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20close=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/static/js/articles.js | 2 +- website/scriptfan/templates/articles/_form.html | 2 +- website/scriptfan/views/articles.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/website/scriptfan/static/js/articles.js b/website/scriptfan/static/js/articles.js index 93bf432..96b062b 100644 --- a/website/scriptfan/static/js/articles.js +++ b/website/scriptfan/static/js/articles.js @@ -13,7 +13,7 @@ $(function() { // 文章标签编辑器 $('.tags-manager').tagsManager({ prefilled: prefilled_tags || [], - shiddenTagListName: 'tags_text' + hiddenTagListName: 'tags_text' }); $('.tags-manager-wrapper').on('click', function() { diff --git a/website/scriptfan/templates/articles/_form.html b/website/scriptfan/templates/articles/_form.html index b5ff883..8401a15 100644 --- a/website/scriptfan/templates/articles/_form.html +++ b/website/scriptfan/templates/articles/_form.html @@ -15,7 +15,7 @@
      - +
      diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 93a5558..2e8daf4 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -39,8 +39,10 @@ def show(article_id): def create(): form = ArticleForm() if form.validate_on_submit(): + app.logger.info('Create new article %s', form.title.data) article = Article() form.populate_obj(article) + app.logger.info(' Tagged as %s', form.tags_text.data) article.author_id = current_user.user.id db.session.add(article) db.session.commit() From 7661e6fcffd1567b216217c60b4f80da58d64770 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Tue, 23 Apr 2013 20:43:14 +0800 Subject: [PATCH 092/119] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E6=96=87=E7=AB=A0=E6=97=B6=E6=9B=B4=E6=96=B0=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=20#16?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/models/article.py | 2 +- website/scriptfan/views/articles.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/website/scriptfan/models/article.py b/website/scriptfan/models/article.py index 40078ba..dd1067a 100644 --- a/website/scriptfan/models/article.py +++ b/website/scriptfan/models/article.py @@ -43,7 +43,7 @@ def tags_text(self, value): # 仅当 tags 发生变化时才进行重新分配 if value != self.tags_text: if self.id: - self.tags.delete() + self.tags[:] = [] for tag_name in value.split(','): tag = Tag.query.filter_by(name=tag_name).first() or Tag(name=tag_name) diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 2e8daf4..63ccf2e 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -45,7 +45,7 @@ def create(): app.logger.info(' Tagged as %s', form.tags_text.data) article.author_id = current_user.user.id db.session.add(article) - db.session.commit() + # db.session.commit() flash('Add article successfully!', 'success') return redirect(url_for('.show', article_id=article.id)) @@ -58,8 +58,11 @@ def update(article_id): article = Article.query.get(article_id) form = ArticleForm(obj=article) if form.validate_on_submit(): + app.logger.info('Updating article: #%s %s', article.id, article.title) + app.logger.info(' Old tags: %s', article.tags_text) form.populate_obj(article) - db.session.commit() + app.logger.info(' New tags: %s', article.tags_text) + # db.session.commit() flash('Update article successfully!', 'success') return redirect(url_for('.index')) From 02cc3372d8263e85ed9cdf3053b1e35a5c9efaee Mon Sep 17 00:00:00 2001 From: greatghoul Date: Tue, 23 Apr 2013 21:14:52 +0800 Subject: [PATCH 093/119] =?UTF-8?q?=E6=96=87=E7=AB=A0=E6=8C=89=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E8=BF=87=E6=BB=A4=20#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/manage.py | 2 +- website/scriptfan/static/css/styles.css | 2 ++ .../templates/articles/_sidebar.html | 8 +++---- .../scriptfan/templates/articles/index.html | 4 ++-- .../zh_CN/LC_MESSAGES/messages.pot | 4 ++-- website/scriptfan/views/articles.py | 22 ++++++++++++------- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/website/manage.py b/website/manage.py index 042c9ad..47a7c97 100644 --- a/website/manage.py +++ b/website/manage.py @@ -36,7 +36,7 @@ def translate(lang): print 'Scanning translations under path:', trans_dir for language in dirs: message_dir = os.path.join(trans_dir, language, 'LC_MESSAGES') - message_source = os.path.join(message_dir, 'messages.po') + message_source = os.path.join(message_dir, 'messages.pot') message_target = os.path.join(message_dir, 'messages.mo') if os.path.exists(message_source): print ' Translateing', language, 'messages...' diff --git a/website/scriptfan/static/css/styles.css b/website/scriptfan/static/css/styles.css index 46a6092..a840da9 100644 --- a/website/scriptfan/static/css/styles.css +++ b/website/scriptfan/static/css/styles.css @@ -55,3 +55,5 @@ body > div.navbar > div.navbar-inner > div.container > div > form > input { .sidebar-list { padding: 9px 0; } + +.nav-header { font-size: 12px; } diff --git a/website/scriptfan/templates/articles/_sidebar.html b/website/scriptfan/templates/articles/_sidebar.html index f217603..0dcc892 100644 --- a/website/scriptfan/templates/articles/_sidebar.html +++ b/website/scriptfan/templates/articles/_sidebar.html @@ -1,10 +1,8 @@ diff --git a/website/scriptfan/templates/articles/index.html b/website/scriptfan/templates/articles/index.html index 618960d..d2de57c 100644 --- a/website/scriptfan/templates/articles/index.html +++ b/website/scriptfan/templates/articles/index.html @@ -1,11 +1,11 @@ {% extends "articles/layout.html" %} -{% block title %}{{ _('articles') }}{% endblock %} +{% block title %}{%- if tag_name -%}{{ 'Tag: ' ~ tag_name}}{%- else -%}{{ _('articles') }}{%- endif %}{% endblock %} {% block content_body %}
        diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.pot b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.pot index 0864d75..947ce17 100644 --- a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.pot +++ b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.pot @@ -53,8 +53,8 @@ msgstr "发布文章" msgid "articles.update" msgstr "编辑文章" -msgid "views.articles.categories" -msgstr "文章分类" +msgid "views.articles.tag_list" +msgstr "标签列表" msgid "event" msgstr "活动" diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 63ccf2e..f5906f3 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -22,16 +22,21 @@ @blueprint.route('/', methods=['GET']) -def index(): - articles = Article.query.order_by('created_time DESC') \ - .paginate(get_page(), app.config.get('PAGE_SIZE', 10)) - return render_template('articles/index.html', articles=articles) +@blueprint.route('/tag/', methods=['GET']) +def index(tag_name=None): + articles = Article.query + if tag_name: + articles = articles.filter(Article.tags.any(name=tag_name)) + articles = articles.order_by('created_time DESC').paginate(get_page(), app.config.get('PAGE_SIZE', 10)) + tags = Tag.query.all() + return render_template('articles/index.html', articles=articles, tags=tags, tag_name=tag_name) @blueprint.route('/', methods=['GET']) def show(article_id): article = Article.query.get(article_id) - return render_template('articles/show.html', article=article) + tags = Tag.query.all() + return render_template('articles/show.html', article=article, tags=tags) @blueprint.route('/create', methods=['GET', 'POST']) @@ -49,7 +54,8 @@ def create(): flash('Add article successfully!', 'success') return redirect(url_for('.show', article_id=article.id)) - return render_template('articles/new.html', form=form) + tags = Tag.query.all() + return render_template('articles/new.html', form=form, tags=tags) @blueprint.route('/edit/', methods=['GET', 'POST']) @@ -66,5 +72,5 @@ def update(article_id): flash('Update article successfully!', 'success') return redirect(url_for('.index')) - app.logger.info(form.data) - return render_template('articles/edit.html', form=form) + tags = Tag.query.all() + return render_template('articles/edit.html', form=form, tags=tags) From 2e10df77022b452e617c04b5b7b68dcb276d08f9 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Tue, 23 Apr 2013 21:45:29 +0800 Subject: [PATCH 094/119] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=96=87=E7=AB=A0?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=8A=9F=E8=83=BD=20close=20#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/static/css/styles.css | 4 ++++ .../scriptfan/templates/articles/show.html | 7 ++++++- .../zh_CN/LC_MESSAGES/messages.pot | 6 ++++++ website/scriptfan/views/articles.py | 20 ++++++++++++++----- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/website/scriptfan/static/css/styles.css b/website/scriptfan/static/css/styles.css index a840da9..c19c492 100644 --- a/website/scriptfan/static/css/styles.css +++ b/website/scriptfan/static/css/styles.css @@ -52,6 +52,10 @@ body > div.navbar > div.navbar-inner > div.container > div > form > input { background: #eeeeee, } +.page-header .btn { + margin-left: 10px; +} + .sidebar-list { padding: 9px 0; } diff --git a/website/scriptfan/templates/articles/show.html b/website/scriptfan/templates/articles/show.html index 20ee15e..6891721 100644 --- a/website/scriptfan/templates/articles/show.html +++ b/website/scriptfan/templates/articles/show.html @@ -6,6 +6,12 @@
        @@ -18,7 +24,6 @@

        {{ article.title }}

        - {# TODO: Use article.content_html instead of content #} {{ article.content_html | safe }}
      diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.pot b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.pot index 947ce17..9d12215 100644 --- a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.pot +++ b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.pot @@ -41,6 +41,12 @@ msgstr "发布文章" msgid "articles.edit" msgstr "修改文章" +msgid "articles.destroy" +msgstr "删除文章" + +msgid "articles.destroy.confirm" +msgstr "确认要删除该文章吗?" + msgid "articles.update %(title)s" msgstr "修改 %(title)s" diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index f5906f3..0300174 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -14,6 +14,7 @@ from scriptfan import db from scriptfan.functions import get_page +from scriptfan.forms.base import RedirectForm from scriptfan.forms.articles import ArticleForm from scriptfan.models import Article, Tag @@ -22,7 +23,7 @@ @blueprint.route('/', methods=['GET']) -@blueprint.route('/tag/', methods=['GET']) +@blueprint.route('/tag//', methods=['GET']) def index(tag_name=None): articles = Article.query if tag_name: @@ -32,14 +33,15 @@ def index(tag_name=None): return render_template('articles/index.html', articles=articles, tags=tags, tag_name=tag_name) -@blueprint.route('/', methods=['GET']) +@blueprint.route('//', methods=['GET']) def show(article_id): article = Article.query.get(article_id) tags = Tag.query.all() - return render_template('articles/show.html', article=article, tags=tags) + destroy_form = RedirectForm() + return render_template('articles/show.html', article=article, tags=tags, destroy_form=destroy_form) -@blueprint.route('/create', methods=['GET', 'POST']) +@blueprint.route('/create/', methods=['GET', 'POST']) @login.login_required def create(): form = ArticleForm() @@ -58,7 +60,7 @@ def create(): return render_template('articles/new.html', form=form, tags=tags) -@blueprint.route('/edit/', methods=['GET', 'POST']) +@blueprint.route('/edit//', methods=['GET', 'POST']) @login.login_required def update(article_id): article = Article.query.get(article_id) @@ -74,3 +76,11 @@ def update(article_id): tags = Tag.query.all() return render_template('articles/edit.html', form=form, tags=tags) + +@blueprint.route('//destroy/', methods=['POST']) +def destroy(article_id): + article = Article.query.get(article_id) + flash('Destroy article successfully!', 'success') + db.session.delete(article) + return redirect(url_for('.index')) + From 2c665168e0c4bc500b6aba4defc068d3b85686c7 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 24 Apr 2013 02:01:16 +0800 Subject: [PATCH 095/119] =?UTF-8?q?=E6=90=AD=E5=BB=BA=E7=AE=80=E5=8D=95?= =?UTF-8?q?=E7=9A=84=E6=9D=83=E9=99=90=E7=B3=BB=E7=BB=9F=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/__init__.py | 39 ++++++++++++++++++++++++++--- website/scriptfan/forms/user.py | 2 ++ website/scriptfan/models/user.py | 2 +- website/scriptfan/permissions.py | 16 ++++++++++++ website/scriptfan/views/articles.py | 3 ++- website/scriptfan/views/users.py | 4 +++ 6 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 website/scriptfan/permissions.py diff --git a/website/scriptfan/__init__.py b/website/scriptfan/__init__.py index ba3a27a..81e4aa3 100644 --- a/website/scriptfan/__init__.py +++ b/website/scriptfan/__init__.py @@ -7,18 +7,19 @@ """ import os -from flask import Flask, render_template, abort, url_for +from flask import Flask, render_template, abort, url_for, session from flask.ext.openid import OpenID from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.login import LoginManager from flask.ext.babel import Babel +from flask.ext.principal import Principal # Create extension instances oid = OpenID() db = SQLAlchemy() login_manager = LoginManager() babel = Babel() - +principals = Principal() # Create flask application instance instance_path = os.path.abspath(os.path.dirname(__file__)) @@ -33,13 +34,15 @@ def config_app(app, config): app.logger.info('- Loading config file: %s' % config) app.config.from_pyfile(config) - + app.logger.info('- Setting up extensions...') db.init_app(app) + config_principle(app) oid.init_app(app) login_manager.init_app(app) babel.init_app(app) + @app.after_request def after_request(response): try: @@ -49,6 +52,36 @@ def after_request(response): abort(500) return response +def config_principle(app): + principals.init_app(app) + + # 配置 priciple + from flask.ext.principal import identity_loaded, RoleNeed + + @identity_loaded.connect_via(app) + def on_identity_loaded(sender, identity): + app.logger.info('----------------') + from flask.ext.login import current_user + if hasattr(current_user, 'user'): + identity.user = current_user.user + if hasattr(current_user.user, 'privilege'): + identity.provides.add(RoleNeed(current_user.user.privilege)) + app.logger.info(current_user.user.privilege) + app.logger.info(identity.provides) + + import pprint + pprint.pprint(identity) + + @principals.identity_loader + def loadIdentityFromSession(): + if 'identity' in session: + return session.get('identity') + + @principals.identity_saver + def save_identity(identity): + session['identity'] = identity + + def dispatch_handlers(app): d = {} diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index 2b58293..bb83035 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -93,3 +93,5 @@ class ManageOpenIDForm(RedirectForm): wtf.AnyOf(['add', 'delete'], message=u'不支持该操作'), ]) provider = wtf.HiddenField('provider', validators=[ wtf.AnyOf(COMMON_PROVIDERS, message=u'还不能绑定到该OpenID'), ]) + + diff --git a/website/scriptfan/models/user.py b/website/scriptfan/models/user.py index 5b6a134..bf61552 100644 --- a/website/scriptfan/models/user.py +++ b/website/scriptfan/models/user.py @@ -45,7 +45,7 @@ class User(db.Model): created_time = db.Column(db.DateTime, default=datetime.now) #: 上次更新资料的时间 updated_time = db.Column(db.DateTime, default=datetime.now) - #: 简单的权限控制,控制级别:3-普通用户 4-管理员 (目前就这么简单,后面再讨论) + #: 简单的权限控制,控制级别:1-被封用户 2-保留用户 3-普通用户 4-普通管理员 5-超级管理员 privilege = db.Column(db.Integer, default=3) #: 用户 openid 的绑定列表 diff --git a/website/scriptfan/permissions.py b/website/scriptfan/permissions.py new file mode 100644 index 0000000..617cdb8 --- /dev/null +++ b/website/scriptfan/permissions.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +""" + scriptfan.permissions + ~~~~~~~~~~~~~~~~~~~~~~ + scriptfan 的权限定义 + 1-被封用户 2-保留用户 3-普通用户 4-普通管理员 5-超级管理员 +""" + +from flask.ext.principal import RoleNeed, Permission + +banned = Permission(RoleNeed(1)) +suspend = Permission(RoleNeed(2)) +user = Permission(RoleNeed(3)) +admin = Permission(RoleNeed(4)) +root = Permission(RoleNeed(5)) + diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 0300174..2ec624c 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -17,7 +17,7 @@ from scriptfan.forms.base import RedirectForm from scriptfan.forms.articles import ArticleForm from scriptfan.models import Article, Tag - +from scriptfan import permissions blueprint = Blueprint("articles", __name__) @@ -42,6 +42,7 @@ def show(article_id): @blueprint.route('/create/', methods=['GET', 'POST']) +@permissions.user.require() @login.login_required def create(): form = ArticleForm() diff --git a/website/scriptfan/views/users.py b/website/scriptfan/views/users.py index 093c3d0..3751406 100644 --- a/website/scriptfan/views/users.py +++ b/website/scriptfan/views/users.py @@ -6,6 +6,7 @@ from flask.ext import login from flask.ext.login import current_user from flask.ext.openid import COMMON_PROVIDERS +from flask.ext.principal import Identity, AnonymousIdentity, identity_changed, identity_loaded from scriptfan import db, oid, login_manager from scriptfan.models import User, UserOpenID from scriptfan.forms.user import SignupForm, SigninForm, EditProfileForm, \ @@ -143,6 +144,7 @@ def signin(): if form.validate_on_submit(): app.logger.info('Signin users: %s', form.email.data) login_user(form.user, remember=form.remember) + identity_changed.send(app._get_current_object(), identity=Identity(current_user.user.id)) flash(u'登陆成功', 'success') return form.redirect('users.profile') @@ -317,5 +319,7 @@ def signout(): login.logout_user() if 'openid_provider' in session: del session['openid_provider'] + identity_changed.send(app._get_current_object(), identity=AnonymousIdentity()) return redirect(url_for('home.index')) + From 33638c1521c2adfe87596928997dd023efa9d308 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Fri, 3 May 2013 22:13:18 +0800 Subject: [PATCH 096/119] =?UTF-8?q?=E6=9D=83=E9=99=90=20-=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=AA=8C=E8=AF=81=E6=9D=83=E9=99=90=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E4=BB=A5=E5=9C=A8=20jinaja2=20=E4=B8=AD=E4=BD=BF?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/__init__.py | 15 ++++++++------- website/scriptfan/functions.py | 11 +++++++++++ website/scriptfan/templates/articles/index.html | 2 ++ website/scriptfan/views/users.py | 6 ++++-- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/website/scriptfan/__init__.py b/website/scriptfan/__init__.py index 81e4aa3..b7ee9c1 100644 --- a/website/scriptfan/__init__.py +++ b/website/scriptfan/__init__.py @@ -37,7 +37,7 @@ def config_app(app, config): app.logger.info('- Setting up extensions...') db.init_app(app) - config_principle(app) + config_pricipal(app) oid.init_app(app) login_manager.init_app(app) babel.init_app(app) @@ -52,7 +52,7 @@ def after_request(response): abort(500) return response -def config_principle(app): +def config_pricipal(app): principals.init_app(app) # 配置 priciple @@ -60,17 +60,14 @@ def config_principle(app): @identity_loaded.connect_via(app) def on_identity_loaded(sender, identity): - app.logger.info('----------------') from flask.ext.login import current_user - if hasattr(current_user, 'user'): + if hasattr(current_user, 'user') and current_user.user.id: identity.user = current_user.user if hasattr(current_user.user, 'privilege'): identity.provides.add(RoleNeed(current_user.user.privilege)) app.logger.info(current_user.user.privilege) app.logger.info(identity.provides) - - import pprint - pprint.pprint(identity) + @principals.identity_loader def loadIdentityFromSession(): @@ -124,3 +121,7 @@ def register_jinja_env(app): app.logger.info('Register jinja variables...') app.jinja_env.globals['static'] = (lambda filename: \ url_for('static', filename=filename)) + from scriptfan.functions import require + app.jinja_env.globals['require'] = require + + diff --git a/website/scriptfan/functions.py b/website/scriptfan/functions.py index ab56040..d3d99e4 100644 --- a/website/scriptfan/functions.py +++ b/website/scriptfan/functions.py @@ -9,6 +9,7 @@ import hashlib from urlparse import urlparse, urljoin from flask import request +from scriptfan import permissions def md5(password): return hashlib.md5(password).hexdigest() @@ -34,3 +35,13 @@ def get_redirect_target(): continue if is_safe_url(target): return target + +def require(*roles): + for role_name in roles: + if hasattr(permissions, role_name): + print getattr(permissions, role_name) + if getattr(permissions, role_name).require(): + return True + + return False + diff --git a/website/scriptfan/templates/articles/index.html b/website/scriptfan/templates/articles/index.html index d2de57c..747130c 100644 --- a/website/scriptfan/templates/articles/index.html +++ b/website/scriptfan/templates/articles/index.html @@ -4,7 +4,9 @@ {% block content_body %} diff --git a/website/scriptfan/views/users.py b/website/scriptfan/views/users.py index 3751406..7d8b91f 100644 --- a/website/scriptfan/views/users.py +++ b/website/scriptfan/views/users.py @@ -317,8 +317,10 @@ def editemail(): @login.login_required def signout(): login.logout_user() - if 'openid_provider' in session: - del session['openid_provider'] + + for key in ('openid_provider', 'identity.name', 'identity.auth_type', 'identity'): + session.pop(key, None) + identity_changed.send(app._get_current_object(), identity=AnonymousIdentity()) return redirect(url_for('home.index')) From 145873a8f9d151b7d23e2aeb0858d8538a63db25 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sat, 4 May 2013 09:24:17 +0800 Subject: [PATCH 097/119] =?UTF-8?q?=E5=8F=91=E8=A1=A8=E6=96=87=E7=AB=A0?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=8A=A0=E5=85=A5=E6=9D=83=E9=99=90=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=20closes=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/__init__.py | 6 +-- website/scriptfan/functions.py | 37 ++++++++++++++++--- website/scriptfan/permissions.py | 10 ++--- .../scriptfan/templates/articles/index.html | 2 +- .../zh_CN/LC_MESSAGES/messages.pot | 4 ++ website/scriptfan/views/articles.py | 4 +- website/scriptfan/views/users.py | 1 - 7 files changed, 47 insertions(+), 17 deletions(-) diff --git a/website/scriptfan/__init__.py b/website/scriptfan/__init__.py index b7ee9c1..474fc61 100644 --- a/website/scriptfan/__init__.py +++ b/website/scriptfan/__init__.py @@ -64,7 +64,7 @@ def on_identity_loaded(sender, identity): if hasattr(current_user, 'user') and current_user.user.id: identity.user = current_user.user if hasattr(current_user.user, 'privilege'): - identity.provides.add(RoleNeed(current_user.user.privilege)) + identity.provides.add(RoleNeed(unicode(current_user.user.privilege))) app.logger.info(current_user.user.privilege) app.logger.info(identity.provides) @@ -121,7 +121,7 @@ def register_jinja_env(app): app.logger.info('Register jinja variables...') app.jinja_env.globals['static'] = (lambda filename: \ url_for('static', filename=filename)) - from scriptfan.functions import require - app.jinja_env.globals['require'] = require + from scriptfan.functions import require_roles + app.jinja_env.globals['require_roles'] = require_roles diff --git a/website/scriptfan/functions.py b/website/scriptfan/functions.py index d3d99e4..b8afeac 100644 --- a/website/scriptfan/functions.py +++ b/website/scriptfan/functions.py @@ -7,8 +7,12 @@ """ import hashlib +from functools import wraps from urlparse import urlparse, urljoin -from flask import request + +from flask import request, flash, redirect, url_for +from flask.ext.babel import gettext as _ + from scriptfan import permissions def md5(password): @@ -32,16 +36,39 @@ def get_page(): def get_redirect_target(): for target in request.args.get('next'), request.referrer: if not target: - continue + return url_for('home.index') if is_safe_url(target): return target -def require(*roles): +def require_roles(*roles): for role_name in roles: if hasattr(permissions, role_name): print getattr(permissions, role_name) - if getattr(permissions, role_name).require(): + if getattr(permissions, role_name).can(): return True return False - + + +def roles_required(*roles): + """ + 此方法用于在 action 上建立权限验证,用法如下 :: + + @app.route('/some_action') + @roles_required('admin', 'root') + def some_action(): + return 'Foo' + + """ + + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if require_roles(*roles): + return f(*args, **kwargs) + else: + flash(_('messages.permission_denied'), 'error') + return redirect(get_redirect_target()) + + return decorated_function + return decorator diff --git a/website/scriptfan/permissions.py b/website/scriptfan/permissions.py index 617cdb8..78e6c0d 100644 --- a/website/scriptfan/permissions.py +++ b/website/scriptfan/permissions.py @@ -8,9 +8,9 @@ from flask.ext.principal import RoleNeed, Permission -banned = Permission(RoleNeed(1)) -suspend = Permission(RoleNeed(2)) -user = Permission(RoleNeed(3)) -admin = Permission(RoleNeed(4)) -root = Permission(RoleNeed(5)) +banned = Permission(RoleNeed(u'1')) +suspend = Permission(RoleNeed(u'2')) +user = Permission(RoleNeed(u'3')) +admin = Permission(RoleNeed(u'4')) +root = Permission(RoleNeed(u'5')) diff --git a/website/scriptfan/templates/articles/index.html b/website/scriptfan/templates/articles/index.html index 747130c..11813d2 100644 --- a/website/scriptfan/templates/articles/index.html +++ b/website/scriptfan/templates/articles/index.html @@ -4,7 +4,7 @@ {% block content_body %}
      diff --git a/website/scriptfan/templates/articles/show.html b/website/scriptfan/templates/articles/show.html index 56ee995..a4d1720 100644 --- a/website/scriptfan/templates/articles/show.html +++ b/website/scriptfan/templates/articles/show.html @@ -21,7 +21,13 @@

      {{ article.title }}

      From 8f270198f965433350e2636aae818cc84f1e8497 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 8 May 2013 00:23:57 +0800 Subject: [PATCH 101/119] =?UTF-8?q?=E7=BA=A0=E6=AD=A3=E5=9B=BD=E9=99=85?= =?UTF-8?q?=E5=8C=96=E6=96=87=E4=BB=B6=E5=91=BD=E5=90=8D=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/manage.py | 2 +- .../zh_CN/LC_MESSAGES/{messages.pot => messages.po} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename website/scriptfan/translations/zh_CN/LC_MESSAGES/{messages.pot => messages.po} (100%) diff --git a/website/manage.py b/website/manage.py index 47a7c97..042c9ad 100644 --- a/website/manage.py +++ b/website/manage.py @@ -36,7 +36,7 @@ def translate(lang): print 'Scanning translations under path:', trans_dir for language in dirs: message_dir = os.path.join(trans_dir, language, 'LC_MESSAGES') - message_source = os.path.join(message_dir, 'messages.pot') + message_source = os.path.join(message_dir, 'messages.po') message_target = os.path.join(message_dir, 'messages.mo') if os.path.exists(message_source): print ' Translateing', language, 'messages...' diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.pot b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po similarity index 100% rename from website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.pot rename to website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po From 0910079de88dc7227e62015530ee127deca7491a Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 8 May 2013 00:37:35 +0800 Subject: [PATCH 102/119] =?UTF-8?q?=E6=A0=87=E7=AD=BE=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E5=9B=BD=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/templates/articles/index.html | 6 +++++- .../scriptfan/translations/zh_CN/LC_MESSAGES/messages.po | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/website/scriptfan/templates/articles/index.html b/website/scriptfan/templates/articles/index.html index e865e88..cd1d292 100644 --- a/website/scriptfan/templates/articles/index.html +++ b/website/scriptfan/templates/articles/index.html @@ -1,6 +1,6 @@ {% extends "articles/layout.html" %} -{% block title %}{%- if tag_name -%}{{ 'Tag: ' ~ tag_name}}{%- else -%}{{ _('articles') }}{%- endif %}{% endblock %} +{% block title %}{%- if tag_name -%}{{ _('views.articles.index.title_tag') % tag_name }}{%- else -%}{{ _('articles') }}{%- endif %}{% endblock %} {% block content_body %}
    +{% if articles.items %} {{ p.render_pagination(articles, 'articles.index') }} +{% else %} +
    {{ _('messages.no_record') % _('articles') }}
    +{% endif %} {% endblock %} diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po index 7f9191f..e575e73 100644 --- a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po +++ b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po @@ -8,6 +8,9 @@ msgstr "" msgid "messages.permission_denied" msgstr "没有权限!" +msgid "messages.no_record" +msgstr "还没有%%s记录!" + ## Users msgid "views.users.slug.title" @@ -66,6 +69,9 @@ msgstr "编辑文章" msgid "views.articles.tag_list" msgstr "标签列表" +msgid "views.articles.index.title_tag" +msgstr "标签:%%s" + msgid "event" msgstr "活动" From 3c0056b95434cc482b0576219c61b6cdfb28f86f Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 8 May 2013 00:44:29 +0800 Subject: [PATCH 103/119] =?UTF-8?q?=E4=BF=AE=E6=AD=A3principal=E6=8B=BC?= =?UTF-8?q?=E5=86=99=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/__init__.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/website/scriptfan/__init__.py b/website/scriptfan/__init__.py index 474fc61..8d8c78e 100644 --- a/website/scriptfan/__init__.py +++ b/website/scriptfan/__init__.py @@ -32,17 +32,16 @@ def config_app(app, config): app.logger.info('Setting up application...') - app.logger.info('- Loading config file: %s' % config) + app.logger.info('Loading config file: %s' % config) app.config.from_pyfile(config) - app.logger.info('- Setting up extensions...') + app.logger.info('Setting up extensions...') db.init_app(app) - config_pricipal(app) + config_principal(app) oid.init_app(app) login_manager.init_app(app) babel.init_app(app) - @app.after_request def after_request(response): try: @@ -52,7 +51,7 @@ def after_request(response): abort(500) return response -def config_pricipal(app): +def config_principal(app): principals.init_app(app) # 配置 priciple @@ -124,4 +123,3 @@ def register_jinja_env(app): from scriptfan.functions import require_roles app.jinja_env.globals['require_roles'] = require_roles - From cf32cff5fb7e05f191f0f9fdd485ee8481be3297 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sun, 2 Jun 2013 19:19:21 +0800 Subject: [PATCH 104/119] =?UTF-8?q?articles=E8=A1=A8=E4=B8=AD=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0published=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../versions/11fb09f6dba5_article_draft.py | 21 +++++++++++++++++++ website/scriptfan/models/article.py | 1 + 2 files changed, 22 insertions(+) create mode 100644 website/migrate/versions/11fb09f6dba5_article_draft.py diff --git a/website/migrate/versions/11fb09f6dba5_article_draft.py b/website/migrate/versions/11fb09f6dba5_article_draft.py new file mode 100644 index 0000000..99b84b8 --- /dev/null +++ b/website/migrate/versions/11fb09f6dba5_article_draft.py @@ -0,0 +1,21 @@ +"""article draft + +Revision ID: 11fb09f6dba5 +Revises: 50321cae8ba5 +Create Date: 2013-06-02 19:11:42.968000 + +""" + +# revision identifiers, used by Alembic. +revision = '11fb09f6dba5' +down_revision = '50321cae8ba5' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('articles', sa.Column('published', sa.Boolean(), nullable=False)) + +def downgrade(): + op.drop_column('articles', 'published') diff --git a/website/scriptfan/models/article.py b/website/scriptfan/models/article.py index cdaef03..da1aaa9 100644 --- a/website/scriptfan/models/article.py +++ b/website/scriptfan/models/article.py @@ -31,6 +31,7 @@ class Article(db.Model): author_id = db.Column(db.Integer, db.ForeignKey('users.id')) created_time = db.Column(db.DateTime, default=datetime.now) updated_time = db.Column(db.DateTime, default=datetime.now) + published = db.Column(db.Boolean, nullable=False) tags = db.relationship('Tag', secondary=article_tags, backref=db.backref('articles', lazy='dynamic')) From 7fb65c6fc524ba961d37235a2586d27f699e1151 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sun, 2 Jun 2013 20:07:50 +0800 Subject: [PATCH 105/119] =?UTF-8?q?=E6=96=87=E7=AB=A0=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E8=8D=89=E7=A8=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/forms/articles.py | 5 +++-- website/scriptfan/templates/articles/_form.html | 3 ++- .../scriptfan/translations/zh_CN/LC_MESSAGES/messages.po | 6 ++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/website/scriptfan/forms/articles.py b/website/scriptfan/forms/articles.py index 49ac609..158e0cb 100644 --- a/website/scriptfan/forms/articles.py +++ b/website/scriptfan/forms/articles.py @@ -4,7 +4,7 @@ scriptfan/forms/articles ~~~~~~~~~~~~~~~~~~~~~~~~ - Forms for articles and categories + 文章相关的表单 """ from flask.ext import wtf @@ -12,8 +12,9 @@ class ArticleForm(RedirectForm): - """ Form for article create and update """ + """ 文章发布和修改的表单 """ title = wtf.TextField('title', validators=[wtf.Required(message=u'请填写标题')]) content = wtf.TextAreaField('content', validators=[wtf.Required(message=u'文章内容不能为空')]) tags_text = wtf.HiddenField('tags_text') + published = wtf.IntegerField('published') diff --git a/website/scriptfan/templates/articles/_form.html b/website/scriptfan/templates/articles/_form.html index 8401a15..44314ca 100644 --- a/website/scriptfan/templates/articles/_form.html +++ b/website/scriptfan/templates/articles/_form.html @@ -23,7 +23,8 @@
    - + +
    diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po index e575e73..298dfbb 100644 --- a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po +++ b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po @@ -45,6 +45,12 @@ msgstr "标题" msgid "articles.create" msgstr "发布文章" +msgid "articles.do_draft" +msgstr "存为草稿" + +msgid "articles.do_publish" +msgstr "发布文章" + msgid "articles.edit" msgstr "修改文章" From 4a8fe8f516e06380e4afbe36b7013a626c234fea Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sun, 2 Jun 2013 20:22:32 +0800 Subject: [PATCH 106/119] =?UTF-8?q?=E5=8F=AA=E6=9C=89=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E6=89=8D=E8=83=BD=E7=9C=8B=E5=88=B0=E6=9C=AA=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E7=9A=84=E6=96=87=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/functions.py | 1 - website/scriptfan/views/articles.py | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/website/scriptfan/functions.py b/website/scriptfan/functions.py index b8afeac..65f25da 100644 --- a/website/scriptfan/functions.py +++ b/website/scriptfan/functions.py @@ -43,7 +43,6 @@ def get_redirect_target(): def require_roles(*roles): for role_name in roles: if hasattr(permissions, role_name): - print getattr(permissions, role_name) if getattr(permissions, role_name).can(): return True diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 4596e34..5dc070d 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -13,7 +13,7 @@ from flask.ext.babel import gettext as _ from scriptfan import db -from scriptfan.functions import get_page, roles_required +from scriptfan.functions import get_page, roles_required, require_roles from scriptfan.forms.base import RedirectForm from scriptfan.forms.articles import ArticleForm from scriptfan.models import Article, Tag @@ -26,8 +26,14 @@ @blueprint.route('/tag//', methods=['GET']) def index(tag_name=None): articles = Article.query + + # 对于非管理员,只能看到发布过的文章 + if not require_roles('admin', 'root'): + articles = articles.filter_by(published=1) + if tag_name: articles = articles.filter(Article.tags.any(name=tag_name)) + articles = articles.order_by('created_time DESC').paginate(get_page(), app.config.get('PAGE_SIZE', 10)) tags = Tag.query.all() return render_template('articles/index.html', articles=articles, tags=tags, tag_name=tag_name) From 26cb0d78711fe2bc4332e6c519c60222a008c5d4 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Thu, 6 Jun 2013 22:48:05 +0800 Subject: [PATCH 107/119] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20flask-mail=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/requirements.txt b/requirements.txt index 30b7d5d..fab0dfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,4 @@ -Babel==0.9.6 Flask==0.9 -Flask-Admin==1.0.4 Flask-Babel==0.8 Flask-Login==0.1.3 Flask-OpenID==1.1.1 @@ -8,18 +6,7 @@ Flask-Principal==0.3.4 Flask-SQLAlchemy==0.16 Flask-Script==0.5.3 Flask-WTF==0.8.3 -Jinja2==2.6 -Mako==0.7.3 -MarkupSafe==0.15 -MySQL-python==1.2.4 -SQLAlchemy==0.8.0 -WTForms==1.0.3 -Werkzeug==0.8.3 +Flask-Mail>=0.8.2 alembic==0.4.2 -blinker==1.2 markdown2==2.1.0 mysql-connector-python==1.0.9 -python-openid==2.2.5 -pytz==2013b -speaklater==1.3 -wsgiref==0.1.2 From a64b427de664ef16d3582c47630b8cccbe00eec0 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Fri, 7 Jun 2013 08:23:27 +0800 Subject: [PATCH 108/119] =?UTF-8?q?=E5=9C=A8=E9=A1=B9=E7=9B=AE=E4=B8=AD?= =?UTF-8?q?=E5=8A=A0=E5=85=A5=20Flask-Mail=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/__init__.py | 3 +++ website/scriptfan/scriptfan.cfg.sample | 10 ++++++++++ website/scriptfan/views/articles.py | 2 ++ 3 files changed, 15 insertions(+) diff --git a/website/scriptfan/__init__.py b/website/scriptfan/__init__.py index 8d8c78e..489b59a 100644 --- a/website/scriptfan/__init__.py +++ b/website/scriptfan/__init__.py @@ -11,6 +11,7 @@ from flask.ext.openid import OpenID from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.login import LoginManager +from flask_mail import Mail from flask.ext.babel import Babel from flask.ext.principal import Principal @@ -20,6 +21,7 @@ login_manager = LoginManager() babel = Babel() principals = Principal() +mail = Mail() # Create flask application instance instance_path = os.path.abspath(os.path.dirname(__file__)) @@ -41,6 +43,7 @@ def config_app(app, config): oid.init_app(app) login_manager.init_app(app) babel.init_app(app) + mail.init_app(app) @app.after_request def after_request(response): diff --git a/website/scriptfan/scriptfan.cfg.sample b/website/scriptfan/scriptfan.cfg.sample index 0eb315a..f6f9c2c 100644 --- a/website/scriptfan/scriptfan.cfg.sample +++ b/website/scriptfan/scriptfan.cfg.sample @@ -23,3 +23,13 @@ BABEL_DEFAULT_LOCALE='zh_CN' # 密码盐,用于增加密码的安全性 PASSWORD_SALT = 'udontkown@must#complicated' + +# 邮件设置 +MAIL_SERVER = 'your mail server' +MAIL_PORT = 25 +MAIL_USE_TLS = False +MAIL_USE_SSL = False +MAIL_USERNAME = 'username' +MAIL_PASSWORD = 'password' +MAIL_DEFAULT_SENDER = 'from@gmail.com' + diff --git a/website/scriptfan/views/articles.py b/website/scriptfan/views/articles.py index 5dc070d..2f13ddb 100644 --- a/website/scriptfan/views/articles.py +++ b/website/scriptfan/views/articles.py @@ -11,8 +11,10 @@ from flask.ext import login from flask.ext.login import current_user from flask.ext.babel import gettext as _ +from flask_mail import Message from scriptfan import db +from scriptfan import mail from scriptfan.functions import get_page, roles_required, require_roles from scriptfan.forms.base import RedirectForm from scriptfan.forms.articles import ArticleForm From ab642155de11b80f5f8845c748067fe6f13681d2 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Mon, 17 Jun 2013 23:21:36 +0800 Subject: [PATCH 109/119] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=87=8D=E7=BD=AE?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E9=82=AE=E4=BB=B6=E5=8F=91=E9=80=81=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/forms/user.py | 9 +++++ .../templates/users/reset_step1.html | 33 +++++++++++++++++++ website/scriptfan/templates/users/signin.html | 1 + .../zh_CN/LC_MESSAGES/messages.po | 3 ++ website/scriptfan/views/users.py | 6 +++- 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 website/scriptfan/templates/users/reset_step1.html diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index bb83035..676723c 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -33,6 +33,15 @@ def validate(self): return not self.errors +class ResetStep1Form(RedirectForm): + """请求发送密码重置邮件的表单""" + + email = wtf.TextField('email', validators=[ + wtf.Required(message=u'请填写电子邮件'), + wtf.Email(message=u'无效的电子邮件')]) + captcha = wtf.TextField('captcha', validators=[ + wtf.Required(message=u'请填写验证码')]) + class SignupForm(RedirectForm): email = wtf.TextField('email', validators=[ diff --git a/website/scriptfan/templates/users/reset_step1.html b/website/scriptfan/templates/users/reset_step1.html new file mode 100644 index 0000000..1975145 --- /dev/null +++ b/website/scriptfan/templates/users/reset_step1.html @@ -0,0 +1,33 @@ +{% extends "layout.html" %} + +{% block title %}{{ _('views.users.reset.title') }}{% endblock %} + +{% block content %} +
    + + +
    + {{ form.hidden_tag() }} +
    + +
    + {{ form.email }} + {{ form.email | error_text }} +
    +
    +
    + +
    + {{ form.captcha(class='input-small') }} + {{ form.captcha | error_text }} +
    +
    +
    + +
    +
    +
    +{% endblock %} + diff --git a/website/scriptfan/templates/users/signin.html b/website/scriptfan/templates/users/signin.html index d39e0dc..6bbb5b2 100644 --- a/website/scriptfan/templates/users/signin.html +++ b/website/scriptfan/templates/users/signin.html @@ -22,6 +22,7 @@

    {{ self.title() }}

    {{ form.password }} {{ form.password | error_text }} + 忘记密码?
    diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po index 298dfbb..80b6198 100644 --- a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po +++ b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po @@ -31,6 +31,9 @@ msgstr "修改密码" msgid "views.users.openids.title" msgstr "管理服务绑定" +msgid "views.users.reset.title" +msgstr "重置密码" + msgid "index" diff --git a/website/scriptfan/views/users.py b/website/scriptfan/views/users.py index d5c790f..c13fd81 100644 --- a/website/scriptfan/views/users.py +++ b/website/scriptfan/views/users.py @@ -11,7 +11,7 @@ from scriptfan.models import User, UserOpenID from scriptfan.forms.user import SignupForm, SigninForm, EditProfileForm, \ EditPasswordForm, EditSlugForm, \ - ManageOpenIDForm + ManageOpenIDForm, ResetStep1Form blurprint = Blueprint('users', __name__) @@ -292,6 +292,10 @@ def slug(): form.process(obj=current_user.user) return render_template('users/slug.html', form=form, skip_slug_info=True) +@blurprint.route('/reset/step1', methods=['GET', 'POST']) +def reset_step1(): + form = ResetStep1Form() + return render_template('users/reset_step1.html', form=form) @blurprint.route('/password', methods=['GET', 'POST']) @login.login_required From 62dbc250ccd6939d92f08d2c98b58346275ea815 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Tue, 18 Jun 2013 01:01:27 +0800 Subject: [PATCH 110/119] =?UTF-8?q?=E5=8F=91=E9=80=81=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E9=82=AE=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/forms/user.py | 10 ++++- .../templates/users/reset_email.html | 11 ++++++ .../templates/users/reset_step1.html | 2 + website/scriptfan/views/users.py | 38 ++++++++++++++++++- 4 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 website/scriptfan/templates/users/reset_email.html diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index 676723c..7bf0922 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -39,8 +39,14 @@ class ResetStep1Form(RedirectForm): email = wtf.TextField('email', validators=[ wtf.Required(message=u'请填写电子邮件'), wtf.Email(message=u'无效的电子邮件')]) - captcha = wtf.TextField('captcha', validators=[ - wtf.Required(message=u'请填写验证码')]) + # captcha = wtf.TextField('captcha', validators=[ + # wtf.Required(message=u'请填写验证码')]) + + def validate_email(form, field): + form.user = User.get_by_email(field.data) + if not form.user: + raise wtf.ValidationError(u'该邮件尚未在本站注册') + class SignupForm(RedirectForm): diff --git a/website/scriptfan/templates/users/reset_email.html b/website/scriptfan/templates/users/reset_email.html new file mode 100644 index 0000000..86abbeb --- /dev/null +++ b/website/scriptfan/templates/users/reset_email.html @@ -0,0 +1,11 @@ +{%- set reset_url = url_for('users.reset_step2', email=user.email, token=token, _external=True) -%} +{%- set home_url = url_for('home.index', _external=True) -%} +{{ user.nickname }},你好! + +

    请点击下面链接进行密码重置:

    + +{{ reset_url }} + +
    + +

    想了解更多信息,请访问 {{ home_url }}

    diff --git a/website/scriptfan/templates/users/reset_step1.html b/website/scriptfan/templates/users/reset_step1.html index 1975145..fc0a45d 100644 --- a/website/scriptfan/templates/users/reset_step1.html +++ b/website/scriptfan/templates/users/reset_step1.html @@ -17,6 +17,7 @@

    {{ self.title() }}

    {{ form.email | error_text }}
    + {#
    @@ -24,6 +25,7 @@

    {{ self.title() }}

    {{ form.captcha | error_text }}
    + #}
    diff --git a/website/scriptfan/views/users.py b/website/scriptfan/views/users.py index c13fd81..6c267ff 100644 --- a/website/scriptfan/views/users.py +++ b/website/scriptfan/views/users.py @@ -1,5 +1,6 @@ #-*-coding:utf-8-*- from datetime import datetime +import uuid from flask import Blueprint, session, request, url_for, redirect, abort, g from flask import render_template, flash from flask import current_app as app @@ -7,7 +8,9 @@ from flask.ext.login import current_user from flask.ext.openid import COMMON_PROVIDERS from flask.ext.principal import Identity, AnonymousIdentity, identity_changed, identity_loaded -from scriptfan import db, oid, login_manager +from flask_mail import Message + +from scriptfan import db, oid, mail, login_manager from scriptfan.models import User, UserOpenID from scriptfan.forms.user import SignupForm, SigninForm, EditProfileForm, \ EditPasswordForm, EditSlugForm, \ @@ -292,11 +295,44 @@ def slug(): form.process(obj=current_user.user) return render_template('users/slug.html', form=form, skip_slug_info=True) + +def _send_reset_email(user, token): + """ 发送密码重置邮件 """ + + try: + app.logger.info('Sending reset email to %s with token %s', user.email, token) + msg = Message(u'ScriptFan密码重置', recipients=[user.email]) + msg.html = render_template('users/reset_email.html', user=user, token=token) + mail.send(msg) + app.logger.info('Mail sent successfully.') + return True + except Exception, e: + app.logger.info(e.message) + app.logger.error('Failed to send password reset mail, because: %s', e) + return False + + @blurprint.route('/reset/step1', methods=['GET', 'POST']) def reset_step1(): form = ResetStep1Form() + if form.validate_on_submit(): + app.logger.info('User %s request to reset password.', form.user.nickname) + token = uuid.uuid4().hex + + if _send_reset_email(form.user, token): + session['reset_token'] = token + flash(u'密码重置邮件已经发送至 %s 请前往收件箱查收。' % form.user.email, 'success') + return form.redirect('home.index') + + flash(u'邮件发送失败,请稍候再试', 'error') + return render_template('users/reset_step1.html', form=form) + +@blurprint.route('/reset/step2', methods=['GET', 'POST']) +def reset_step2(): + return 'Hello World!' + @blurprint.route('/password', methods=['GET', 'POST']) @login.login_required def password(): From 5ceba2254467dcd11037952a24e76c4c5cdcc052 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Tue, 18 Jun 2013 01:21:22 +0800 Subject: [PATCH 111/119] =?UTF-8?q?=E6=B7=BB=E5=8A=A0FIXME?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/views/users.py | 1 + 1 file changed, 1 insertion(+) diff --git a/website/scriptfan/views/users.py b/website/scriptfan/views/users.py index 6c267ff..d55247e 100644 --- a/website/scriptfan/views/users.py +++ b/website/scriptfan/views/users.py @@ -303,6 +303,7 @@ def _send_reset_email(user, token): app.logger.info('Sending reset email to %s with token %s', user.email, token) msg = Message(u'ScriptFan密码重置', recipients=[user.email]) msg.html = render_template('users/reset_email.html', user=user, token=token) + # FIXME: 邮件发送使用异步方式,避免用户等待太长时间 mail.send(msg) app.logger.info('Mail sent successfully.') return True From 99b050252af5f1d12a658db320c970c7da4bf5a8 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Tue, 18 Jun 2013 08:32:49 +0800 Subject: [PATCH 112/119] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E9=82=AE=E4=BB=B6?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E5=AF=86=E7=A0=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/forms/user.py | 12 +++++- .../templates/users/reset_step2.html | 35 +++++++++++++++++ website/scriptfan/views/users.py | 38 ++++++++++++++++++- 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 website/scriptfan/templates/users/reset_step2.html diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index 7bf0922..71a7e06 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -48,6 +48,16 @@ def validate_email(form, field): raise wtf.ValidationError(u'该邮件尚未在本站注册') +class ResetStep2Form(RedirectForm): + """ 接收密码重置邮件后重新填写密码 """ + + password = wtf.PasswordField(u'新密码', validators=[ + wtf.Required(message=u'请填写新密码,不能少与5位字符'), + wtf.Length(min=5, max=20, message=u'密码应为5到20位字符')]) + confirm = wtf.PasswordField(u'确认密码', validators=[ + wtf.Required(message=u'请再次输入新密码'), + wtf.EqualTo('password', message=u'两次输入的密码不一致')]) + class SignupForm(RedirectForm): email = wtf.TextField('email', validators=[ @@ -91,7 +101,7 @@ class EditPasswordForm(RedirectForm): wtf.EqualTo('password', message=u'两次输入的密码不一致')]) def validate_old_password(form, field): - # 当用户密码为空时,跳过原始密码验证,是否存在安全隐患? + # FIXME: 当用户密码为空时,跳过原始密码验证,是否存在安全隐患? if current_user.user.password and (not current_user.user.check_password(field.data)): raise wtf.ValidationError(u'提供的原始密码不正确') diff --git a/website/scriptfan/templates/users/reset_step2.html b/website/scriptfan/templates/users/reset_step2.html new file mode 100644 index 0000000..b69ad10 --- /dev/null +++ b/website/scriptfan/templates/users/reset_step2.html @@ -0,0 +1,35 @@ +{% extends "layout.html" %} + +{% block title %}{{ _('views.users.reset.title') }}{% endblock %} + +{% block content %} +
    + + +
    + {{ form.hidden_tag() }} +
    + +
    + {{ form.password }} + {{ form.password | error_text }} +
    +
    + +
    + +
    + {{ form.confirm }} + {{ form.confirm | error_text }} +
    +
    + +
    + +
    +
    +
    +{% endblock %} + diff --git a/website/scriptfan/views/users.py b/website/scriptfan/views/users.py index d55247e..2ced9ff 100644 --- a/website/scriptfan/views/users.py +++ b/website/scriptfan/views/users.py @@ -14,7 +14,7 @@ from scriptfan.models import User, UserOpenID from scriptfan.forms.user import SignupForm, SigninForm, EditProfileForm, \ EditPasswordForm, EditSlugForm, \ - ManageOpenIDForm, ResetStep1Form + ManageOpenIDForm, ResetStep1Form, ResetStep2Form blurprint = Blueprint('users', __name__) @@ -315,12 +315,16 @@ def _send_reset_email(user, token): @blurprint.route('/reset/step1', methods=['GET', 'POST']) def reset_step1(): + """ 重置密码第一步,发送重置链接邮件 """ + + # TODO: 相关文字的国际化处理 form = ResetStep1Form() if form.validate_on_submit(): app.logger.info('User %s request to reset password.', form.user.nickname) token = uuid.uuid4().hex if _send_reset_email(form.user, token): + session['reset_email'] = form.user.email session['reset_token'] = token flash(u'密码重置邮件已经发送至 %s 请前往收件箱查收。' % form.user.email, 'success') return form.redirect('home.index') @@ -329,10 +333,40 @@ def reset_step1(): return render_template('users/reset_step1.html', form=form) +def _valid_reset_token(): + """ 验证重置口令及邮件地址是否匹配 """ + + return session.get('reset_email') and \ + session.get('reset_token') and \ + session['reset_email'] == request.args['email'] and \ + session['reset_token'] == request.args['token'] @blurprint.route('/reset/step2', methods=['GET', 'POST']) def reset_step2(): - return 'Hello World!' + form = ResetStep2Form() + if form.validate_on_submit(): + app.logger.info("Updating to new password") + + # FIXME: 密码重置 - 验证用户不存在的情况是否有必要? + user = User.get_by_email(session['reset_email']) + user.set_password(form.password.data) + flash(u'用户密码已经更新', 'success') + + # TODO: 通过重置密码功能修改密码成功后,向用户发送邮件提醒。 + + # 清除验证用的 Token + del session['reset_email'] + del session['reset_token'] + + return redirect(url_for('users.signin')) + + # 如果邮件及当前token均匹配,则显示重置密码的表单 + if _valid_reset_token(): + return render_template('users/reset_step2.html', form=form) + else: + flash(u'重置密码链接无效或者已经过期,请重新发送重置密码邮件', 'warning') + return redirect(url_for('users.reset_step1')) + @blurprint.route('/password', methods=['GET', 'POST']) @login.login_required From 86e1bf091ddde01a5adbcaf57045d0aa863d6707 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sat, 22 Jun 2013 21:49:52 +0800 Subject: [PATCH 113/119] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/templates/users/signup.html | 8 +++++++- .../scriptfan/translations/zh_CN/LC_MESSAGES/messages.po | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/website/scriptfan/templates/users/signup.html b/website/scriptfan/templates/users/signup.html index 0b8b907..226e3be 100644 --- a/website/scriptfan/templates/users/signup.html +++ b/website/scriptfan/templates/users/signup.html @@ -38,8 +38,14 @@

    {{ self.title() }}

    {{ form.password2 | error_text }}
    +
    - +
    +

    注册协议

    +

    要注册成为本站会员,体重大概大于 80 公斤。

    +
    + +   或者直接 使用Google登陆
    diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po index 80b6198..0a858f0 100644 --- a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po +++ b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po @@ -12,6 +12,8 @@ msgid "messages.no_record" msgstr "还没有%%s记录!" ## Users +msgid "views.users.signup.confirm_signup" +msgstr "同意用户协议并注册" msgid "views.users.slug.title" msgstr "设置个性域名" From 0cf54e7613d76e03f85ac0a6fa88cbc95cefbbad Mon Sep 17 00:00:00 2001 From: greatghoul Date: Sat, 22 Jun 2013 22:23:09 +0800 Subject: [PATCH 114/119] =?UTF-8?q?Fix=20#33=20=E7=99=BB=E9=99=86=E6=97=B6?= =?UTF-8?q?,=20=E5=A6=82=E6=9E=9C=E9=82=AE=E4=BB=B6=E5=9C=B0=E5=9D=80/?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E9=94=99=E8=AF=AF,=20=E9=83=BD=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E9=82=AE=E4=BB=B6/=E5=AF=86=E7=A0=81=E9=94=99?= =?UTF-8?q?=E8=AF=AF=EF=BC=9B=E8=A1=A5=E5=85=A8=E7=99=BB=E9=99=86=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=9B=BD=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/forms/user.py | 50 ++++++++++--------- website/scriptfan/templates/users/signin.html | 14 +++--- .../zh_CN/LC_MESSAGES/messages.po | 42 ++++++++++++++++ website/scriptfan/views/users.py | 3 +- website/start.bat | 1 + 5 files changed, 79 insertions(+), 31 deletions(-) diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index 71a7e06..59cc5ef 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -8,26 +8,47 @@ from flask.ext import wtf from flask.ext.login import current_user +from flask.ext.babel import gettext as _ from scriptfan.models import User from scriptfan.forms.base import RedirectForm from flask.ext.openid import COMMON_PROVIDERS -class SigninForm(RedirectForm): + +class SignupForm(RedirectForm): + """用户注册表单""" + email = wtf.TextField('email', validators=[ wtf.Required(message=u'请填写电子邮件'), wtf.Email(message=u'无效的电子邮件')]) - password = wtf.PasswordField('password', validators=[ + nickname = wtf.TextField('nickname', validators=[ + wtf.Required(message=u'请填写昵称')]) + password1 = wtf.PasswordField('password1', validators=[ wtf.Required(message=u'请填写密码')]) + password2 = wtf.PasswordField('password2', validators=[ + wtf.Required(message=u'再次填写密码'), + wtf.EqualTo('password1', message=u'两次输入的密码不一致')]) + + def validate_email(form, field): + if User.get_by_email(field.data): + raise wtf.ValidationError(u'该邮箱已被注册') + + +class SigninForm(RedirectForm): + """ 用户登陆表单 """ + + email = wtf.TextField('email', validators=[ + wtf.Required(message=_('forms.signin.errors.require_email')), + wtf.Email(message=_('forms.signin.errors.invalid_email'))]) + password = wtf.PasswordField('password', validators=[ + wtf.Required(message=_('forms.signin.errors.require_password'))]) remember = wtf.BooleanField('remember') def validate(self): if not super(RedirectForm, self).validate(): return False user = User.get_by_email(self.email.data) - if not user: - self.email.errors.append(u'该邮箱未注册') - elif not user.check_password(self.password.data): - self.password.errors.append(u'密码错误') + if not user or user.check_password(self.password.data): + self.email.errors.append(_('forms.signin.errors.invalid_email_or_password')) else: self.user = user @@ -59,23 +80,6 @@ class ResetStep2Form(RedirectForm): wtf.EqualTo('password', message=u'两次输入的密码不一致')]) -class SignupForm(RedirectForm): - email = wtf.TextField('email', validators=[ - wtf.Required(message=u'请填写电子邮件'), - wtf.Email(message=u'无效的电子邮件')]) - nickname = wtf.TextField('nickname', validators=[ - wtf.Required(message=u'请填写昵称')]) - password1 = wtf.PasswordField('password1', validators=[ - wtf.Required(message=u'请填写密码')]) - password2 = wtf.PasswordField('password2', validators=[ - wtf.Required(message=u'再次填写密码'), - wtf.EqualTo('password1', message=u'两次输入的密码不一致')]) - - def validate_email(form, field): - if User.get_by_email(field.data): - raise wtf.ValidationError(u'该邮箱已被注册') - - class EditProfileForm(RedirectForm): nickname = wtf.TextField('nickname', validators=[ wtf.Required(message=u'请填写昵称')]) diff --git a/website/scriptfan/templates/users/signin.html b/website/scriptfan/templates/users/signin.html index 6bbb5b2..50189fb 100644 --- a/website/scriptfan/templates/users/signin.html +++ b/website/scriptfan/templates/users/signin.html @@ -1,6 +1,6 @@ {% extends "layout.html" %} -{% block title %}会员登陆{% endblock %} +{% block title %}{{ _('views.users.signin.title') }}{% endblock %} {% block content %}
    @@ -11,26 +11,26 @@

    {{ self.title() }}

    {{ form.hidden_tag() }}
    - +
    {{ form.email }} {{ form.email | error_text }}
    - +
    {{ form.password }} {{ form.password | error_text }} - 忘记密码? + {{ _('forms.signin.messages.password_lost') }}
    -    +    -
    使用 Gmail帐户 登陆
    +
    diff --git a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po index 0a858f0..e27cc71 100644 --- a/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po +++ b/website/scriptfan/translations/zh_CN/LC_MESSAGES/messages.po @@ -15,6 +15,12 @@ msgstr "还没有%%s记录!" msgid "views.users.signup.confirm_signup" msgstr "同意用户协议并注册" +msgid "views.users.signin.title" +msgstr "用户登录" + +msgid "views.users.signin.signin_success" +msgstr "登录成功!" + msgid "views.users.slug.title" msgstr "设置个性域名" @@ -109,3 +115,39 @@ msgstr "内容" msgid "submit" msgstr "提交" + +## 实体相关信息 + +msgid "models.user.email" +msgstr "邮箱" + +msgid "models.user.password" +msgstr "密码" + +## 表单及相关信息 + +# SigninForm + +msgid "forms.signin.errors.invalid_email_or_password" +msgstr "邮箱或者密码错误" + +msgid "forms.signin.errors.require_email" +msgstr "请填写邮箱" + +msgid "forms.signin.errors.invalid_email" +msgstr "无效的邮箱" + +msgid "forms.signin.errors.require_password" +msgstr "请填写密码" + +msgid "forms.signin.fields.remember" +msgstr "记住我的登陆信息" + +msgid "forms.signin.buttons.submit" +msgstr "登录" + +msgid "forms.signin.messages.password_lost" +msgstr "忘记密码?" + +msgid "links.signin_with_google" +msgstr "使用Google帐户登陆" diff --git a/website/scriptfan/views/users.py b/website/scriptfan/views/users.py index 2ced9ff..42dcd51 100644 --- a/website/scriptfan/views/users.py +++ b/website/scriptfan/views/users.py @@ -7,6 +7,7 @@ from flask.ext import login from flask.ext.login import current_user from flask.ext.openid import COMMON_PROVIDERS +from flask.ext.babel import gettext as _ from flask.ext.principal import Identity, AnonymousIdentity, identity_changed, identity_loaded from flask_mail import Message @@ -147,7 +148,7 @@ def signin(): app.logger.info('Signin users: %s', form.email.data) login_user(form.user, remember=form.remember) identity_changed.send(app._get_current_object(), identity=Identity(current_user.user.id)) - flash(u'登陆成功', 'success') + flash(_('views.users.signin.signin_success'), 'success') return form.redirect('users.profile') return render_template('users/signin.html', form=form) diff --git a/website/start.bat b/website/start.bat index 1a02a4e..2f7189b 100644 --- a/website/start.bat +++ b/website/start.bat @@ -2,3 +2,4 @@ title ScriptFan Development Server echo Staring ScriptFan dev server... python manage.py runserver -H localhost +pause From 648a130fc0a3d9ea4957c9e35f8d480955b98049 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Tue, 25 Jun 2013 00:07:38 +0800 Subject: [PATCH 115/119] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=82=AE=E4=BB=B6?= =?UTF-8?q?=E5=8F=91=E9=80=81=E5=8A=9F=E8=83=BD=20#39?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 用户注册后发送欢迎邮件 - 重置密码成功后发送通知邮件 - 优化发送密码模板的结构 - 修正登陆时密码验证逻辑错误的问题 --- website/scriptfan/forms/user.py | 3 +- .../{reset_email.html => email/reset.html} | 0 .../templates/users/email/reset_success.html | 6 +++ .../templates/users/email/welcome.html | 4 ++ website/scriptfan/views/users.py | 45 +++++++++++++++---- 5 files changed, 49 insertions(+), 9 deletions(-) rename website/scriptfan/templates/users/{reset_email.html => email/reset.html} (100%) create mode 100644 website/scriptfan/templates/users/email/reset_success.html create mode 100644 website/scriptfan/templates/users/email/welcome.html diff --git a/website/scriptfan/forms/user.py b/website/scriptfan/forms/user.py index 59cc5ef..8581d03 100644 --- a/website/scriptfan/forms/user.py +++ b/website/scriptfan/forms/user.py @@ -6,6 +6,7 @@ 定义用户相关页面所用到的表单, 包括注册、登陆、基本资料修改、密码修改、邮箱修改等。 """ +from flask import current_app as app from flask.ext import wtf from flask.ext.login import current_user from flask.ext.babel import gettext as _ @@ -47,7 +48,7 @@ def validate(self): if not super(RedirectForm, self).validate(): return False user = User.get_by_email(self.email.data) - if not user or user.check_password(self.password.data): + if not user or not user.check_password(self.password.data): self.email.errors.append(_('forms.signin.errors.invalid_email_or_password')) else: self.user = user diff --git a/website/scriptfan/templates/users/reset_email.html b/website/scriptfan/templates/users/email/reset.html similarity index 100% rename from website/scriptfan/templates/users/reset_email.html rename to website/scriptfan/templates/users/email/reset.html diff --git a/website/scriptfan/templates/users/email/reset_success.html b/website/scriptfan/templates/users/email/reset_success.html new file mode 100644 index 0000000..a9b0f78 --- /dev/null +++ b/website/scriptfan/templates/users/email/reset_success.html @@ -0,0 +1,6 @@ +{%- set home_url = url_for('home.index', _external=True) -%} +{{ user.nickname }},你好! + +你的 ScriptFan 的密码已经修改。 + +

    想了解更多信息,请访问 {{ home_url }}

    diff --git a/website/scriptfan/templates/users/email/welcome.html b/website/scriptfan/templates/users/email/welcome.html new file mode 100644 index 0000000..4b2c378 --- /dev/null +++ b/website/scriptfan/templates/users/email/welcome.html @@ -0,0 +1,4 @@ +{%- set home_url = url_for('home.index', _external=True) -%} +{{ user.nickname }},你好! + +

    想了解更多信息,请访问 {{ home_url }}

    diff --git a/website/scriptfan/views/users.py b/website/scriptfan/views/users.py index 42dcd51..fbeba9a 100644 --- a/website/scriptfan/views/users.py +++ b/website/scriptfan/views/users.py @@ -218,6 +218,8 @@ def _create_or_login(resp): db.session.add(user) db.session.commit() + _send_welcome_email(user) + flash(u'帐号已经创建, 可以在资料修改页面补充密码等信息'% url_for('users.general'), 'success') app.logger.info('Signin users: %s', user.email) @@ -240,6 +242,7 @@ def signup(): user.set_password(form.password1.data) db.session.add(user) app.logger.info(u'New users added: %s', user) + _send_welcome_email(user) flash(u'注册成功', 'success') return redirect(url_for('users.signin')) @@ -279,8 +282,6 @@ def general(): # TODO 处理更新用户资料的请求 # TODO 用户照片上传 -# TODO: 用户找回密码功能 - # 更新用户slug功能 @blurprint.route('/slug', methods=['GET', 'POST']) @login.login_required @@ -296,6 +297,19 @@ def slug(): form.process(obj=current_user.user) return render_template('users/slug.html', form=form, skip_slug_info=True) +def _send_welcome_email(user): + """ 发送欢迎邮件 """ + + try: + app.logger.info('Sending welcome email to %s', user.email) + msg = Message(u'欢迎来到ScriptFan', recipients=[user.email]) + msg.html = render_template('users/email/welcome.html', user=user) + # FIXME: 邮件发送使用异步方式,避免用户等待太长时间 + mail.send(msg) + app.logger.info('Mail sent successfully.') + except Exception, e: + app.logger.info(e.message) + app.logger.error('Failed to send welcome reset mail, because: %s', e) def _send_reset_email(user, token): """ 发送密码重置邮件 """ @@ -303,7 +317,7 @@ def _send_reset_email(user, token): try: app.logger.info('Sending reset email to %s with token %s', user.email, token) msg = Message(u'ScriptFan密码重置', recipients=[user.email]) - msg.html = render_template('users/reset_email.html', user=user, token=token) + msg.html = render_template('users/email/reset.html', user=user, token=token) # FIXME: 邮件发送使用异步方式,避免用户等待太长时间 mail.send(msg) app.logger.info('Mail sent successfully.') @@ -313,6 +327,20 @@ def _send_reset_email(user, token): app.logger.error('Failed to send password reset mail, because: %s', e) return False +def _send_reset_success_email(user): + """ 发送重置通知邮件 """ + + try: + app.logger.info('Sending email confirm notification to %s', user.email) + msg = Message(u'你在 ScriptFan 的密码已经重置!', recipients=[user.email]) + msg.html = render_template('users/email/reset_success.html', user=user) + # FIXME: 邮件发送使用异步方式,避免用户等待太长时间 + mail.send(msg) + app.logger.info('Mail sent successfully.') + except Exception, e: + app.logger.info(e.message) + app.logger.error('Failed to send email reset notification mail, because: %s', e) + @blurprint.route('/reset/step1', methods=['GET', 'POST']) def reset_step1(): @@ -336,11 +364,12 @@ def reset_step1(): def _valid_reset_token(): """ 验证重置口令及邮件地址是否匹配 """ - + email, token = request.args.get('email'), request.args.get('token') + app.logger.info('Validating email reset with email: %s and token: %s', email, token) return session.get('reset_email') and \ session.get('reset_token') and \ - session['reset_email'] == request.args['email'] and \ - session['reset_token'] == request.args['token'] + session['reset_email'] == email and \ + session['reset_token'] == token @blurprint.route('/reset/step2', methods=['GET', 'POST']) def reset_step2(): @@ -353,12 +382,12 @@ def reset_step2(): user.set_password(form.password.data) flash(u'用户密码已经更新', 'success') - # TODO: 通过重置密码功能修改密码成功后,向用户发送邮件提醒。 - # 清除验证用的 Token del session['reset_email'] del session['reset_token'] + _send_reset_success_email(user) + return redirect(url_for('users.signin')) # 如果邮件及当前token均匹配,则显示重置密码的表单 From 513a1c0b52d70a07a000a3c1e94d0ea3ed0a9974 Mon Sep 17 00:00:00 2001 From: greatghoul Date: Tue, 25 Jun 2013 23:19:28 +0800 Subject: [PATCH 116/119] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scriptfan/templates/users/profile.html | 65 +++++++++++++------ 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/website/scriptfan/templates/users/profile.html b/website/scriptfan/templates/users/profile.html index d0a0eb8..94c57b3 100644 --- a/website/scriptfan/templates/users/profile.html +++ b/website/scriptfan/templates/users/profile.html @@ -8,26 +8,51 @@ {% block content %}
    - + + +
    +
    电子邮件:{{ user.email }}
    + {% if user.id == current_user.user.id %} +
    手机号码:{{ user.phone }}
    + {% elif not current_user.is_anonymous() %} + {% if user.phone_privacy in (1, 2) %} +
    手机号码:{{ user.phone }}
    + {% endif %} + {% elif user.phone_privacy == 1 %} +
    手机号码:{{ user.phone }}
    + {% endif %} +
    座右铭:{{ user.motoo }}
    +
    个人介绍:{{ user.intro }}
    +
    注册日期:{{ user.created_time | dateformat }}
    +
    {% endblock %} From ac243d3e2deb7bb17a2cf4d5ec248af0ac3873bd Mon Sep 17 00:00:00 2001 From: greatghoul Date: Wed, 26 Jun 2013 01:23:33 +0800 Subject: [PATCH 117/119] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=BA=A7=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/scriptfan/__init__.py | 2 +- website/scriptfan/models/user.py | 13 ++++ website/scriptfan/static/js/commons.js | 12 +++- website/scriptfan/static/js/user.js | 4 -- website/scriptfan/static/js/users.js | 26 ++++++++ website/scriptfan/templates/layout.html | 4 +- .../scriptfan/templates/users/profile.html | 22 ++++--- .../zh_CN/LC_MESSAGES/messages.po | 17 ++++++ website/scriptfan/views/users.py | 60 ++++++++++++------- 9 files changed, 120 insertions(+), 40 deletions(-) delete mode 100644 website/scriptfan/static/js/user.js create mode 100644 website/scriptfan/static/js/users.js diff --git a/website/scriptfan/__init__.py b/website/scriptfan/__init__.py index 489b59a..76cca89 100644 --- a/website/scriptfan/__init__.py +++ b/website/scriptfan/__init__.py @@ -107,7 +107,7 @@ def register_blueprints(app): app.logger.info('Register blueprints...') from scriptfan.views import home, events, users, articles app.register_blueprint(home.blueprint, url_prefix='') - app.register_blueprint(users.blurprint, url_prefix='/users') + app.register_blueprint(users.blueprint, url_prefix='/users') app.register_blueprint(events.blueprint, url_prefix='/events') app.register_blueprint(articles.blueprint, url_prefix='/articles') diff --git a/website/scriptfan/models/user.py b/website/scriptfan/models/user.py index bf61552..a3136a8 100644 --- a/website/scriptfan/models/user.py +++ b/website/scriptfan/models/user.py @@ -12,6 +12,8 @@ from datetime import datetime from flask import url_for, request +from flask.ext.babel import gettext as _ + class User(db.Model): __tablename__ = 'users' @@ -71,6 +73,17 @@ def get_by_email(cls, email): @classmethod def get_by_slug(cls, slug): return User.query.filter_by(slug=slug).first() + + @classmethod + def get_by_slug_or_id(cls, slug_or_id): + if slug_or_id.isdigit(): + return User.query.get(int(slug_or_id)).first() + else: + return User.query.filter_by(slug=slug_or_id).first() + + @property + def privilege_name(self): + return _('models.user.privilege.privilege%s' % self.privilege) @property def url(self): diff --git a/website/scriptfan/static/js/commons.js b/website/scriptfan/static/js/commons.js index 4fe5896..c85df78 100644 --- a/website/scriptfan/static/js/commons.js +++ b/website/scriptfan/static/js/commons.js @@ -1,6 +1,6 @@ function fmt() { var args = arguments; - return args[0].replace(/%\{(.*?)}/g, function(match, prop) { + return args[0].replace(/%\{(.*?)}/img, function(match, prop) { return function(obj, props) { var prop = /\d+/.test(props[0]) ? parseInt(props[0]) : props[0]; if (props.length > 1) { @@ -11,3 +11,13 @@ function fmt() { }(typeof args[1] === 'object' ? args[1] : args, prop.split(/\.|\[|\]\[|\]\./)); }); } + +function flash(message, level) { + var tpl = '' + + '
    ' + + ' ' + + ' %{2}' + + '
    '; + $(fmt(tpl, level, message)).appendTo('.flash').delay(3000).fadeOut(); +} + diff --git a/website/scriptfan/static/js/user.js b/website/scriptfan/static/js/user.js deleted file mode 100644 index f543c59..0000000 --- a/website/scriptfan/static/js/user.js +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 用户登陆,注册,资料修改等功能 - */ - diff --git a/website/scriptfan/static/js/users.js b/website/scriptfan/static/js/users.js new file mode 100644 index 0000000..702bccb --- /dev/null +++ b/website/scriptfan/static/js/users.js @@ -0,0 +1,26 @@ +/** +* 用户登陆,注册,资料修改等功能 +*/ + +(function($) { + // 更新用户的角色类型 + function updateUserRole() { + $.ajax({ + url: document.location.href, + data: { privilege: $(this).data('privilege') }, + type: 'POST', + complete: function(message) { + document.location.reload(true); + }, + error: function() { + document.location.reload(true); + } + }); + } + + function initialize() { + $(document).on('click', '#role-menus li a', updateUserRole); + } + + $(document).ready(initialize); +})(jQuery); diff --git a/website/scriptfan/templates/layout.html b/website/scriptfan/templates/layout.html index ac13598..1c5a3b4 100644 --- a/website/scriptfan/templates/layout.html +++ b/website/scriptfan/templates/layout.html @@ -14,6 +14,8 @@ {% block styles %} {{ t.css('css/%s.css' % request.blueprint) }} {% endblock %} + + {{ t.js(url_for('home.env'), external=True) }}