diff --git a/development.ini b/development.ini index f2463cf..21b96c1 100644 --- a/development.ini +++ b/development.ini @@ -42,6 +42,7 @@ paths.themes = %(here)s/themes # Security settings security.salt = secretsalt +# Mail server settings mail.on = false mail.manager = immediate mail.transport = smtp @@ -49,6 +50,9 @@ mail.smtp.server = mail.mydomain.com mail.smtp.username = mymailusername mail.smtp.password = mymailpassword +# Server-related settings +server.timezone = 'Africa/Johannesburg' + # WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* # Debug mode will enable the interactive debugging tool, allowing ANYONE to # execute malicious code after an exception is raised. diff --git a/scribeengine/config/routing.py b/scribeengine/config/routing.py index a5cbf09..f8dbc2a 100644 --- a/scribeengine/config/routing.py +++ b/scribeengine/config/routing.py @@ -51,9 +51,12 @@ def make_map(): map.connect('/tag/{id}', controller='blog', action='tag') map.connect('/calendar/{year}/{month}', controller='blog', action='calendar') + map.connect('/{controller}') map.connect('/{controller}/{action}') map.connect('/{controller}/{action}/{id}') + map.connect('/{url}', controller='page', action='view') + map.connect('/', controller='blog', action='index') return map diff --git a/scribeengine/controllers/account.py b/scribeengine/controllers/account.py index 93434de..6545d1a 100644 --- a/scribeengine/controllers/account.py +++ b/scribeengine/controllers/account.py @@ -27,6 +27,7 @@ from urlparse import urlsplit from datetime import datetime from formencode.validators import Int +from pytz import all_timezones from scribeengine.lib.base import * from scribeengine.lib.validation.client import JSString, JSEmail @@ -39,26 +40,94 @@ log = logging.getLogger(__name__) class AccountController(BaseController): + @authenticate() def index(self): - h.redirect_to(h.url_for(controller=u'account', action=u'login')) + c.page_title = u'My Account' + c.timezones = all_timezones + return render(u'/account/index.mako') + + @jsvalidate(u'account-account') + def index_jsschema(self): + return { + u'account-nick': JSString(required=True, + message=u'You need to type in a nick.'), + u'account-email': JSEmail(required=True, + message=u'You need to supply a valid e-mail address.') + } + + def index_schema(self): + return { + 'account-nick': UnicodeString(not_empty=True, + messages={u'empty': u'You need to type in a nick.'}), + 'account-email': Email(not_empty=True, + messages={u'empty': u'You need to supply a valid e-mail address.'}) + } + + def index_POST(self): + try: + for key, value in c.form_values.iteritems(): + setattr(c.current_user, key[8:], value) + Session.add(c.current_user) + Session.commit() + h.flash.set_message(u'Successfully updated your account.', u'success') + except: + h.flash.set_message(u'There was a problem updating your account.', u'error') + h.redirect_to(h.url_for(controller=u'account')) + + @authenticate() + def password(self): + c.page_title = u'Your Password' + return render(u'/account/password.mako') + + @jsvalidate(u'account-password') + def password_jsschema(self): + return { + u'password-password': JSString(required=True, message=u'You haven\'t typed in a password.'), + u'password-confirm': JSString(required=True, equalTo=u'#password-password', message=u'Your passwords don\'t match.') + } + + def password_schema(self): + return { + 'password-password': UnicodeString(not_empty=True, messages={'empty': u'You haven\'t typed in a password.'}), + 'confirm-password': [FieldsMatch('password-password', 'password-confirm', messages={'invalid': u'Your passwords don\'t match.'})] + } + + @authenticate() + def password_POST(self): + password_hash = utils.hash_password(c.form_values[u'password-password']) + log.debug('Old Hash: "%s"', c.current_user.password) + log.debug('New Hash: "%s"', password_hash) + c.current_user.password = password_hash + c.current_user.modified = datetime.now() + Session.add(c.current_user) + Session.commit() + h.flash.set_message(u'Successfully updated your password.', u'success') + h.redirect_to('/account/password') def register(self): c.page_title = u'Register' return render(u'/account/register.mako') - @jsvalidate(u'register-form') + @jsvalidate(u'account-register') def register_jsschema(self): return { - u'register-email': JSEmail(required=True, message=u'You haven\'t typed in an e-mail address.'), - u'register-password': JSString(required=True, message=u'You haven\'t typed in a password.'), - u'register-confirm': JSString(required=True, equalTo=u'#password', message=u'Your passwords don\'t match.') + u'register-email': JSEmail(required=True, + message=u'You haven\'t typed in an e-mail address.'), + u'register-password': JSString(required=True, + message=u'You haven\'t typed in a password.'), + u'register-confirm': JSString(required=True, equalTo=u'#password', + message=u'Your passwords don\'t match.') } def register_schema(self): return { - 'register-email': Email(not_empty=True, messages={'empty': u'You haven\'t typed in an e-mail address.'}), - 'register-password': UnicodeString(not_empty=True, messages={'empty': u'You haven\'t typed in a password.'}), - 'confirm-password': [FieldsMatch('register-password', 'register-confirm', messages={'invalid': u'Your passwords don\'t match.'})] + 'register-email': Email(not_empty=True, + messages={'empty': u'You haven\'t typed in an e-mail address.'}), + 'register-password': UnicodeString(not_empty=True, + messages={'empty': u'You haven\'t typed in a password.'}), + 'confirm-password': [ + FieldsMatch('register-password', 'register-confirm', + messages={'invalid': u'Your passwords don\'t match.'})] } def register_POST(self): @@ -168,7 +237,10 @@ class AccountController(BaseController): u'e-mail.', u'success') h.redirect_to('/account/login') - def password(self, id=None): + def resetpassword(self, id=None): + """ + Reset your password. + """ if not id or not request.GET.get(u'code'): h.flash.set_message(u'There was a problem with your activation code, please reset your password again.', u'error') h.redirect_to(h.url_for(controller=u'account', action=u'login')) @@ -179,33 +251,35 @@ class AccountController(BaseController): if c.user.activation_key != request.GET.get(u'code'): h.flash.set_message(u'There was a problem with your activation code, please reset your password again.', u'error') h.redirect_to(h.url_for(controller=u'account', action=u'login')) - c.page_title = u'Change Password' - return render(u'/account/password.mako') + c.page_title = u'Reset Password' + return render(u'/account/resetpassword.mako') - @jsvalidate(u'account-password') - def password_jsschema(self): + @jsvalidate(u'account-resetpassword') + def resetpassword_jsschema(self): return { u'password-password': JSString(required=True, message=u'You haven\'t typed in a password.'), u'password-confirm': JSString(required=True, equalTo=u'#password-password', message=u'Your passwords don\'t match.') } - def password_schema(self): + def resetpassword_schema(self): return { 'password-password': UnicodeString(not_empty=True, messages={'empty': u'You haven\'t typed in a password.'}), 'confirm-password': [FieldsMatch('password-password', 'password-confirm', messages={'invalid': u'Your passwords don\'t match.'})] } - def password_POST(self, id=None): + def resetpassword_POST(self, id=None): user = Session.query(User).get(id) if not user: - h.flash.set_message(u'There was a problem with your account, please reset your password again.', u'error') + h.flash.set_message(u'There was a problem with your account, ' + u'please reset your password again.', u'error') h.redirect_to(h.url_for(controller=u'account', action=u'login')) user.password = utils.hash_password(c.form_values[u'password-password']) user.activation_key = None user.modified = datetime.now() Session.add(user) Session.commit() - h.flash.set_message(u'Successfully updated your password. Please login with your new password.', u'success') + h.flash.set_message(u'Successfully updated your password. Please login ' + u'with your new password.', u'success') h.redirect_to('/account/login') def login(self): diff --git a/scribeengine/controllers/blog.py b/scribeengine/controllers/blog.py index 774c05f..7b610d2 100644 --- a/scribeengine/controllers/blog.py +++ b/scribeengine/controllers/blog.py @@ -25,6 +25,7 @@ from datetime import datetime from pprint import pformat from sqlalchemy.sql import or_ +from pytz import timezone from scribeengine.lib.base import * from scribeengine.lib import utils @@ -170,7 +171,10 @@ class BlogController(BaseController): def calendar(self, year, month): #c.calendar = Calendar(6) #c.today = datetime.today() - c.thismonth = datetime.now().replace(int(year), int(month)) + server_tz = timezone(config.get(u'server.timezone', u'UTC')) + user_tz = c.current_user.timezone if c.current_user and c.current_user.timezone else u'UTC' + now = datetime.now(server_tz).astimezone(timezone(user_tz)) + c.thismonth = now.replace(int(year), int(month)) c.prev_month = c.thismonth - monthdelta(1) c.next_month = c.thismonth + monthdelta(1) month_start = datetime(c.thismonth.year, c.thismonth.month, 1, 0, 0, 0, 0) diff --git a/scribeengine/controllers/page.py b/scribeengine/controllers/page.py new file mode 100644 index 0000000..da89b25 --- /dev/null +++ b/scribeengine/controllers/page.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# ScribeEngine - Open Source Blog Software # +# --------------------------------------------------------------------------- # +# Copyright (c) 2010 Raoul Snyman # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + +import logging +from datetime import datetime +import time + +from scribeengine.lib.base import * +from scribeengine.lib import utils +from scribeengine.model import Page +from scribeengine.model.meta import Session + +log = logging.getLogger(__name__) + +class PageController(BaseController): + + def index(self): + h.redirect_to('/') + + def view(self, url): + c.page = Session.query(Page)\ + .filter_by(url=url)\ + .first() + #.filter_by(status=u'published')\ + c.page_title = c.page.title + return render(u'/page/view.mako') + + @authenticate(u'Add Pages') + def new(self): + c.page_title = u'New Page' + return render(u'/page/new.mako') + + @authenticate(u'Edit My Pages') + def edit(self, id=None): + if id is None: + h.redirect_to(h.url_for(controller=u'page', action=u'new')) + c.page = Session.query(Page).get(id) + c.page_title = u'Edit Page: %s' % c.page.title + return render(u'/post/edit.mako') + + @authenticate(u'Edit My Pages') + def edit_POST(self, id=None): + url = utils.generate_url(c.form_values[u'page-title']) + if id is None: + page = Page() + page.user = c.current_user + page.created = datetime.now() + else: + page = Session.query(Page).get(id) + page.modified = datetime.now() + page.title = c.form_values[u'page-title'] + page.body = c.form_values[u'page-body'] + page.url = url + Session.add(page) + Session.commit() + h.redirect_to('/' + str(page.url)) diff --git a/scribeengine/lib/base.py b/scribeengine/lib/base.py index d166a30..0071e79 100644 --- a/scribeengine/lib/base.py +++ b/scribeengine/lib/base.py @@ -39,6 +39,7 @@ from pylons.templating import render_mako from sqlalchemy.sql.expression import asc, desc from formencode import Schema, Invalid from monthdelta import monthdelta +from pytz import timezone from scribeengine.lib import helpers as h from scribeengine.lib.validation import jsvalidate @@ -57,9 +58,11 @@ class BaseController(WSGIController): c.categories = Session.query(Category).order_by(Category.name.asc()).all() c.pages = Session.query(Page).all() c.calendar = Calendar(6) - c.today = datetime.today() + server_tz = timezone(config.get(u'server.timezone', u'UTC')) + user_tz = c.current_user.timezone if c.current_user and c.current_user.timezone else u'UTC' + c.today = datetime.now(server_tz).astimezone(timezone(user_tz)) if not c.thismonth: - c.thismonth = datetime.now() + c.thismonth = c.today #datetime.utcnow().astimezone(timezone(c.current_user.timezone)) c.prev_month = c.thismonth - monthdelta(1) c.next_month = c.thismonth + monthdelta(1) month_start = datetime(c.thismonth.year, c.thismonth.month, 1, 0, 0, 0, 0) diff --git a/scribeengine/model/classes.py b/scribeengine/model/classes.py index 69c75d5..3c2bef0 100644 --- a/scribeengine/model/classes.py +++ b/scribeengine/model/classes.py @@ -108,6 +108,12 @@ class User(BaseModel): if perm == permission: return True return False + elif isinstance(permission, list): + for role in self.roles: + for perm in role.permissions: + if perm.name in permission: + return True + return False else: return False diff --git a/scribeengine/model/tables.py b/scribeengine/model/tables.py index 9ef1c4b..c8819ad 100644 --- a/scribeengine/model/tables.py +++ b/scribeengine/model/tables.py @@ -103,6 +103,7 @@ users_table = Table(u'users', metadata, Column(u'first_name', Unicode(100), default=u''), Column(u'last_name', Unicode(100), default=u''), Column(u'homepage', Unicode(200), default=u''), + Column(u'timezone', Unicode(200), default=u'UTC'), Column(u'activation_key', Unicode(40), default=None) ) diff --git a/scribeengine/public/styles/style.css b/scribeengine/public/styles/style.css index 8a3f11a..a78b713 100644 --- a/scribeengine/public/styles/style.css +++ b/scribeengine/public/styles/style.css @@ -393,6 +393,19 @@ fieldset { color: #fff; } +.form-select { + color: #fff; + background-color: #1f1f1f; + font-size: 1.2em; + border: 1px solid #454545; + padding: 3px 5px; +} + +.form-select option { + background-color: #1f1f1f; + color: #fff; +} + .form-text { font-size: 1.5em; padding: 4px 6px; @@ -423,6 +436,22 @@ fieldset { margin-top: 0.2em; } +fieldset.form-details { + background-color: #111; + margin: 4em 0 1.5em; + padding: 10px; +} + +fieldset.form-details legend { + color: #fff; + font-size: 1.5em; + margin: -1.7em 0 0 0; +} + +fieldset.form-details .form-text { + width: 573px; +} + #register-now { margin-left: 1em; } diff --git a/scribeengine/templates/account/index.mako b/scribeengine/templates/account/index.mako new file mode 100644 index 0000000..252a018 --- /dev/null +++ b/scribeengine/templates/account/index.mako @@ -0,0 +1,48 @@ +<%inherit file="/base.mako"/> + <%include file="/flash.mako"/> +
+

My Account

+

This is your account page. Update your details by changing them below and clicking the Save button.

+ <%include file="/errors.mako"/> +
+
+ Required Details +
+ + +
+
+ + +
+
+
+ Personal Details +
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+
+
diff --git a/scribeengine/templates/account/password.mako b/scribeengine/templates/account/password.mako index 3eaa03e..c6cc1df 100644 --- a/scribeengine/templates/account/password.mako +++ b/scribeengine/templates/account/password.mako @@ -1,9 +1,9 @@ <%inherit file="/base.mako"/> <%include file="/flash.mako"/>
-

New password

+

Change password

<%include file="/errors.mako"/> -
+
diff --git a/scribeengine/templates/account/resetpassword.mako b/scribeengine/templates/account/resetpassword.mako new file mode 100644 index 0000000..f72c855 --- /dev/null +++ b/scribeengine/templates/account/resetpassword.mako @@ -0,0 +1,21 @@ +<%inherit file="/base.mako"/> + <%include file="/flash.mako"/> +
+

Reset password

+ <%include file="/errors.mako"/> + +
+
+ + +
+
+ + +
+
+ +
+
+ +
diff --git a/scribeengine/templates/base.mako b/scribeengine/templates/base.mako index e81957c..da03fec 100644 --- a/scribeengine/templates/base.mako +++ b/scribeengine/templates/base.mako @@ -29,17 +29,10 @@
diff --git a/scribeengine/templates/email/reset.mako b/scribeengine/templates/email/reset.mako index 40ab706..529cd6f 100644 --- a/scribeengine/templates/email/reset.mako +++ b/scribeengine/templates/email/reset.mako @@ -4,7 +4,7 @@ You have just reset your password on ${c.blog_title}, but before you continue, you will need to activate your account. You can do this by simply clicking on the link below, or copying and pasting it into your browser. -${c.blog_host}${h.url_for(controller=u'account', action=u'password', id=c.user.id, code=c.user.activation_key)} +${c.blog_host}${h.url_for(controller=u'account', action=u'resetpassword', id=c.user.id, code=c.user.activation_key)} Kind regards, diff --git a/scribeengine/templates/page/new.mako b/scribeengine/templates/page/new.mako new file mode 100644 index 0000000..f49639c --- /dev/null +++ b/scribeengine/templates/page/new.mako @@ -0,0 +1,21 @@ +<%inherit file="/base.mako"/> + <%include file="/flash.mako"/> +
+

New Page

+ <%include file="/errors.mako"/> +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
diff --git a/scribeengine/templates/page/view.mako b/scribeengine/templates/page/view.mako new file mode 100644 index 0000000..2553b9b --- /dev/null +++ b/scribeengine/templates/page/view.mako @@ -0,0 +1,8 @@ +<%inherit file="/base.mako"/> + <%include file="/flash.mako"/> +
+

${c.page.title}

+
+ ${h.literal(c.page.body)} +
+
diff --git a/scribeengine/templates/sidebar.mako b/scribeengine/templates/sidebar.mako index 27243e6..81fd3f7 100644 --- a/scribeengine/templates/sidebar.mako +++ b/scribeengine/templates/sidebar.mako @@ -25,6 +25,39 @@ % endif +
  • +

    Meta

    +
      +% if c.current_user: +
    • + Account + +
    • +% if c.current_user.has_permission([u'Add Posts', u'Add Pages', u'Edit My Posts', u'Edit My Pages']): +
    • + Content +
        +% if c.current_user.has_permission(u'Add Posts'): +
      • New Post
      • +% endif +% if c.current_user.has_permission(u'Add Pages'): +
      • New Page
      • +% endif +% if c.current_user.has_permission(u'Edit My Posts'): +
      • Draft Posts
      • +% endif +
      +
    • +% endif +
    • Logout
    • +% else: +
    • Login
    • +% endif +
    +
  •  
    diff --git a/scribeengine/tests/functional/test_page.py b/scribeengine/tests/functional/test_page.py new file mode 100644 index 0000000..08c7e76 --- /dev/null +++ b/scribeengine/tests/functional/test_page.py @@ -0,0 +1,7 @@ +from scribeengine.tests import * + +class TestPageController(TestController): + + def test_index(self): + response = self.app.get(url(controller='page', action='index')) + # Test response... diff --git a/setup.py b/setup.py index 4342c9f..a0c69cc 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- try: from setuptools import setup, find_packages except ImportError: @@ -28,7 +29,8 @@ setup( "Pylons==0.9.7", "SQLAlchemy>=0.5,<0.6", "TurboMail>=3.0,<3.1", - "MonthDelta>=0.9,<0.10" + "MonthDelta>=0.9,<0.10", + "pytz" ], setup_requires=["PasteScript>=1.6.3"], packages=find_packages(exclude=['ez_setup']),