Nozomu Kaneko avatar Nozomu Kaneko committed d3f4807

initial ver

Comments (0)

Files changed (20)

+==============
 HgWiki README
+==============
+
+1. Install
+
+::
+
+  $ python setup.py develop
+
+2. Setup
+
+::
+
+  $ python hgwiki/fixture.py development.ini
+
+3. Run
+
+::
+
+  $ paster serve --reload development.ini
+
+You can login with the following information:
+
+:username: pyramid
+:password: pylons
 pyramid.debug_notfound = false
 pyramid.debug_routematch = false
 pyramid.debug_templates = true
-pyramid.default_locale_name = en
+pyramid.default_locale_name = ja
 pyramid.includes = pyramid_debugtoolbar
 
+sqlalchemy.url = sqlite:///%(here)s/hgwiki.db
+sqlalchemy.echo = true
+
+jinja2.directories = hgwiki:templates
+
 [server:main]
 use = egg:Paste#http
 host = 0.0.0.0

hgwiki/__init__.py

+# -*- coding: utf-8 -*-
+
 from pyramid.config import Configurator
+from pyramid.session import UnencryptedCookieSessionFactoryConfig
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+
+import sqlahelper
+from sqlalchemy import engine_from_config
+
 from hgwiki.resources import Root
+from hgwiki.security import groupfinder
+from hgwiki import routing
+
+__all__ = ['main']
+
 
 def main(global_config, **settings):
     """ This function returns a Pyramid WSGI application.
     """
-    config = Configurator(root_factory=Root, settings=settings)
-    config.add_view('hgwiki.views.my_view',
-                    context='hgwiki:resources.Root',
-                    renderer='hgwiki:templates/mytemplate.pt')
-    config.add_static_view('static', 'hgwiki:static', cache_max_age=3600)
+    session_factory = UnencryptedCookieSessionFactoryConfig('zzz')
+
+    authn_policy = AuthTktAuthenticationPolicy(
+        'sosecret',
+        callback=groupfinder)
+    authz_policy = ACLAuthorizationPolicy()
+
+    config = Configurator(
+        settings=settings,
+        root_factory=Root,
+        session_factory=session_factory,
+        authentication_policy=authn_policy,
+        authorization_policy=authz_policy)
+
+    config.include('pyramid_tm')
+    config.include('pyramid_jinja2')
+
+    engine = engine_from_config(settings)
+    sqlahelper.add_engine(engine)
+
+    routing.configure(config)
+
     return config.make_wsgi_app()

hgwiki/exceptions.py

+# -*- coding: utf-8 -*-
+
+__all__ = [
+    'WikiError',
+    'PageAlreadyExist',
+    'PageNotFound',
+    ]
+
+
+class WikiError(Exception):
+    pass
+
+class PageAlreadyExist(WikiError):
+    pass
+
+class PageNotFound(WikiError):
+    pass

hgwiki/fixture.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+
+import transaction
+
+from hgwiki import models
+from hgwiki.security import make_digest
+
+default_users = [
+    ("pyramid", "pylons")
+    ]
+
+
+def setup(env):
+    request = env['request']
+    context = env['root']
+
+    models.Base.metadata.create_all()
+
+    if models.DBSession.query(models.User).first() is None:
+        for user_name, password in default_users:
+            digest = make_digest(user_name, password, request)
+            user = models.User(user_name=user_name, password_digest=digest)
+            models.DBSession.add(user)
+        transaction.commit()
+
+    if models.DBSession.query(models.Page).first() is None:
+        root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+        f = open(os.path.join(root, "README.txt"))
+        try:
+            try:
+                content = f.read()
+            except:
+                pass
+            else:
+                context.create_page("README", content)
+        finally:
+            f.close()
+
+
+if __name__ == '__main__':
+    import sys
+    from pyramid.paster import bootstrap
+
+    if len(sys.argv) > 1:
+        config_uri = sys.argv[1]
+    else:
+        config_uri = 'development.ini'
+
+    env = bootstrap(config_uri)
+    setup(env)
+# -*- coding: utf-8 -*-
+
+from formencode.api import Invalid
+from formencode.schema import Schema
+from formencode import validators
+
+__all__ = ['LoginSchema', 'UpdatePageSchema']
+
+
+class LoginSchema(Schema):
+    allow_extra_fields = True
+    filter_extra_fields = True
+    ignore_key_missing = False
+
+    user_name = validators.UnicodeString()
+    password = validators.UnicodeString()
+
+
+class UpdatePageSchema(Schema):
+    allow_extra_fields = True
+    filter_extra_fields = True
+    ignore_key_missing = False
+
+    name = validators.UnicodeString(max=255, not_empty=True)
+    content = validators.UnicodeString()
+# -*- coding: utf-8 -*-
+
+import sqlahelper
+import sqlalchemy as sa
+from sqlalchemy.schema import Column
+from sqlalchemy.orm import relationship
+
+from hgwiki.wiki import substitute_links
+
+__all__ = ['Base', 'DBSession', 'User', 'Page']
+
+Base = sqlahelper.get_base()
+DBSession = sqlahelper.get_session()
+
+
+class User(Base):
+    __tablename__ = 'user'
+
+    id = Column(sa.Integer, primary_key=True)
+    user_name = Column(sa.Unicode(255), unique=True)
+    password_digest = Column(sa.UnicodeText)
+
+
+class Page(Base):
+    __tablename__ = 'page'
+
+    id = Column(sa.Integer, primary_key=True)
+    created_at = Column(sa.DateTime, nullable=False)
+    modified_at = Column(sa.DateTime, nullable=False, index=True)
+    name = Column(sa.Unicode(255), unique=True)
+    text_content = Column(sa.UnicodeText)
+    html_content = Column(sa.UnicodeText)
+    author_id = Column(sa.Integer, sa.ForeignKey('user.id'))
+    author = relationship("User")
+
+    @property
+    def html(self):
+        query = DBSession.query(Page)
+        def exists(name):
+            return query.filter_by(name=name).first() is not None
+
+        return substitute_links(self.html_content, exists)

hgwiki/resources.py

+# -*- coding: utf-8 -*-
+
+import re
+import datetime
+
+from sqlalchemy import sql
+from sqlalchemy.orm.exc import NoResultFound
+import transaction
+
+from pyramid.security import Allow, Everyone, Authenticated
+from pyramid.security import authenticated_userid
+
+from hgwiki import models
+from hgwiki import exceptions as exc
+from hgwiki import wiki
+from hgwiki.security import make_digest
+
+__all__ = ['Root']
+
+
 class Root(object):
+    __acl__ = [(Allow, Everyone, 'view'),
+               (Allow, Authenticated, 'edit')]
+
     def __init__(self, request):
         self.request = request
+
+    def authenticate(self, user_name, password):
+        digest = make_digest(user_name, password, self)
+        query = models.DBSession.query(models.User)
+        try:
+            query.filter_by(user_name=user_name, password_digest=digest).one()
+        except NoResultFound:
+            return False
+        else:
+            return True
+
+    def current_user(self):
+        user_name = authenticated_userid(self.request)
+        return self.get_user(user_name)
+
+    def get_user(self, user_name):
+        query = models.DBSession.query(models.User)
+        try:
+            return query.filter_by(user_name=user_name).one()
+        except NoResultFound:
+            return None
+
+    def make_wiki_name(self, name):
+        name = name.strip()
+        name = name.replace(' ', '_')
+        name = re.sub(r'^_+', '', name)
+        return name
+
+    def get_page(self, name):
+        query = models.DBSession.query(models.Page)
+        try:
+            return query.filter_by(name=name).one()
+        except NoResultFound:
+            return None
+
+    def get_page_names(self):
+        query = models.DBSession.query(models.Page)
+        return [page.name for page in query.all()]
+
+    def get_recent_pages(self, num=20):
+        query = models.DBSession.query(models.Page)
+        query = query.order_by(sql.desc('modified_at'))
+        return query.limit(num).all()
+
+    def create_page(self, name, text):
+        page = self.get_page(name)
+        if page:
+            raise exc.PageAlreadyExist
+
+        html = wiki.convert_text(text)
+        now = datetime.datetime.now()
+
+        page = models.Page(name=name, created_at=now)
+        models.DBSession.add(page)
+
+        page.modified_at = now
+        page.text_content = text
+        page.html_content = html
+        page.author = self.current_user()
+
+        transaction.commit()
+
+    def update_page(self, name, text):
+        page = self.get_page(name)
+        if not page:
+            raise exc.PageNotFound
+
+        html = wiki.convert_text(text)
+        now = datetime.datetime.now()
+
+        page.modified_at = now
+        page.text_content = text
+        page.html_content = html
+        page.author = self.current_user()
+
+        transaction.commit()

hgwiki/routing.py

+# -*- coding: utf-8 -*-
+
+__all__ = ['configure']
+
+
+def configure(config):
+    config.add_route('login', '/login')
+    config.add_view('hgwiki.views.login_view',
+                    route_name='login',
+                    renderer='hgwiki:templates/login.jinja2')
+    config.add_view('hgwiki.views.login_view',
+                    context='pyramid.httpexceptions.HTTPForbidden',
+                    renderer='hgwiki:templates/login.jinja2')
+
+    config.add_route('logout', '/logout')
+    config.add_view('hgwiki.views.logout_view', route_name='logout')
+
+    config.add_route('home', '/', request_method='GET')
+    config.add_view('hgwiki.views.home_view',
+                    route_name='home',
+                    renderer='hgwiki:templates/home.jinja2')
+
+    config.add_route('create_page', '/wiki/', request_method='POST')
+    config.add_view('hgwiki.views.create_page_view',
+                    route_name='create_page',
+                    renderer='hgwiki:templates/new_page.jinja2',
+                    permission='edit')
+
+    config.add_route('new_page', '/wiki/_new', request_method='GET')
+    config.add_view('hgwiki.views.new_page_view',
+                    route_name='new_page',
+                    renderer='hgwiki:templates/new_page.jinja2',
+                    permission='edit')
+
+    config.add_route('show_page', '/wiki/{name}', request_method='GET')
+    config.add_view('hgwiki.views.show_page_view',
+                    route_name='show_page',
+                    renderer='hgwiki:templates/show_page.jinja2')
+
+    config.add_route('update_page', '/wiki/{name}', request_method='POST')
+    config.add_view('hgwiki.views.update_page_view',
+                    route_name='update_page',
+                    renderer='hgwiki:templates/show_page.jinja2')
+
+    config.add_static_view('static', 'hgwiki:static', cache_max_age=3600)

hgwiki/security.py

+# -*- coding: utf-8 -*-
+
+import hashlib
+
+__all__ = ['make_digest', 'groupfinder']
+
+USER_GROUP = "user"
+
+
+def make_digest(user_name, password, request):
+    return hashlib.md5(user_name + password).hexdigest()
+
+
+def groupfinder(userid, request):
+    user = request.context.get_user(userid)
+    if user:
+        return USER_GROUP

hgwiki/templates/base.jinja2

+{%- extends 'layout.jinja2' -%}
+
+{%- set user = request.context.current_user() -%}
+{%- set page_title = "" -%}
+
+{%- block title -%}{{ page_title }}{%- endblock -%}
+
+{% block global_navi -%}
+<a href="{{ request.route_url('home') }}">Home</a>
+{% if user %}
+| Hi, {{ user.user_name }}
+| <a href="{{ request.route_url('logout') }}">Log Out</a>
+{% else %}
+| <a href="{{ request.route_url('login') }}">Log In</a>
+{% endif %}
+{%- endblock -%}
+
+{%- block flash_message -%}
+{% if request.session.peek_flash() -%}
+<div id="flash_message">
+  <ul>
+    {% for m in request.session.pop_flash() -%}
+      <li>{{ m }}</li>
+    {%- endfor %}
+  </ul>
+</div>
+{% endif -%}
+{%- endblock -%}

hgwiki/templates/home.jinja2

+{%- extends 'base.jinja2' -%}
+
+{%- block content -%}
+  <h1>HgWiki</h1>
+
+  <form action="{{ request.route_url('new_page') }}" method="GET">
+    <fieldset>
+      <legend>new page</legend>
+      <input type="input" name="name" />
+      <input type="submit" value="New" />
+    </fieldset>
+  </form>
+
+  <h2>Recent Changes</h2>
+  <ul>
+  {% for page in pages -%}
+    <li><a href="{{ request.route_url('show_page', name=page.name) }}">{{ page.name }}</a> - {{ page.modified_at }} by {{ page.author.user_name if page.author else "Anonymous" }}</li>
+  {%- endfor %}
+  </ul>
+{%- endblock -%}

hgwiki/templates/layout.jinja2

+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
+<head>
+  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
+  <title>{% block title %}{% endblock %}</title>
+  {% block head -%}
+  {%- endblock %}
+</head>
+<body>
+  {% block pagetop %}{% endblock -%}
+  {% block global_navi %}{% endblock -%}
+  {% block flash_message %}{% endblock -%}
+  {% block content %}{% endblock -%}
+  {% block pagebottom %}{% endblock -%}
+</body>
+</html>

hgwiki/templates/login.jinja2

+{%- extends 'base.jinja2' -%}
+
+{%- block content -%}
+  <h1>Login</h1>
+
+  {% if form_error -%}
+    {{ form_error }}
+  {%- endif %}
+  <form action="{{ request.route_url('login') }}" method="POST">
+    <input type="hidden" name="return" value="{{ return_url }}" />
+    <table>
+      <tr>
+        <th><label for="user_name">Username:</label></th>
+        <td><input type="text" id="user_name" name="user_name" value="{{ form_value.user_name }}"/></td>
+      </tr>
+      <tr>
+        <th><label for="password">Password:</label></th>
+        <td><input type="password" id="password" name="password" value="{{ form_value.password }}"/></td>
+      </tr>
+    </table>
+    <input type="submit" name="submit" value="Log In"/>
+  </form>
+{%- endblock -%}

hgwiki/templates/new_page.jinja2

+{%- extends 'base.jinja2' -%}
+
+{%- block content -%}
+  <h1>New Page</h2>
+
+  {% if form_error -%}
+    {{ form_error }}
+  {%- endif %}
+  <form action="{{ request.route_url('create_page') }}" method="POST">
+    <fieldset>
+      <legend>new page</legend>
+      <input type="input" name="name" value="{{ form_value.name }}" /><br />
+      <textarea name="content" cols="80" rows="10">{{ form_value.content }}</textarea><br />
+      <input type="submit" value="Create" />
+    </fieldset>
+  </form>
+{%- endblock -%}

hgwiki/templates/page_created.jinja2

+{%- extends 'base.jinja2' -%}
+
+{%- block content -%}
+  <h1>Page Created</h2>
+  <a href="{{ request.route_url('home') }}">Home</a>
+{%- endblock -%}

hgwiki/templates/show_page.jinja2

+{%- extends 'base.jinja2' -%}
+
+{%- block content -%}
+  <h1>{{ page.name }}</h2>
+  <div id="wiki_content">{{ page.html|safe }}</div>
+  {% if user -%}
+  <form action="{{ request.route_url('update_page', name=page.name) }}" method="POST">
+    <fieldset>
+      <legend>edit page</legend>
+      <input type="hidden" name="name" value="{{ page.name }}" />
+      <textarea name="content" cols="80" rows="10">{{ page.text_content }}</textarea><br />
+      <input type="submit" value="Update" />
+    </fieldset>
+  </form>
+  {%- else -%}
+  <a href="{{ request.route_url('login', _query=dict(return=request.url)) }}">Log in to edit this page</a>
+  {%- endif %}
+{%- endblock -%}
-def my_view(request):
-    return {'project':'HgWiki'}
+# -*- coding: utf-8 -*-
+
+from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPFound
+from pyramid.security import remember, forget
+
+from formencode.api import Invalid
+
+from hgwiki import exceptions as exc
+from hgwiki import forms
+
+__all__ = [
+    'login_view',
+    'logout_view',
+    'home_view',
+    'new_page_view',
+    'create_page_view',
+    'show_page_view',
+    'update_page_view',
+    ]
+
+
+def redirect(request, location, headers=None):
+    return HTTPFound(location=location, headers=headers)
+
+
+def login_view(request):
+    login_url = request.route_url('login')
+    referrer = request.url
+    if referrer == login_url:
+        # never use the login form itself as return_url
+        referrer = request.route_url('home')
+    return_url = request.params.get('return', referrer)
+
+    schema = forms.LoginSchema()
+
+    error = None
+    if request.POST:
+        try:
+            value = schema.to_python(request.params)
+        except Invalid, e:
+            error = e
+        else:
+            user_name = value['user_name']
+            password = value['password']
+            if request.context.authenticate(user_name, password):
+                headers = remember(request, user_name)
+                return redirect(request, return_url, headers)
+        request.session.flash('Failed login')
+
+    return dict(return_url=return_url,
+                form_value=request.params,
+                form_error=error)
+
+
+def logout_view(request):
+    headers = forget(request)
+    return redirect(request, request.route_url('home'), headers)
+
+
+def home_view(request):
+    pages = request.context.get_recent_pages()
+    return dict(pages=pages)
+
+
+def new_page_view(request):
+    page_name = request.params.get('name')
+    safe_page_name = request.context.make_wiki_name(page_name)
+    if page_name != safe_page_name:
+        url = request.route_url('new_page', _query=dict(name=safe_page_name))
+        return redirect(request, url)
+    return dict(form_value=dict(name=page_name))
+
+
+def create_page_view(request):
+    schema = forms.UpdatePageSchema()
+
+    error = None
+    try:
+        value = schema.to_python(request.params)
+    except Invalid, e:
+        error = e
+    else:
+        page_name = value['name']
+        content = value['content']
+        try:
+            request.context.create_page(page_name, content)
+        except exc.PageAlreadyExist:
+            request.session.flash('Page already exist')
+        else:
+            request.session.flash('Successfully created')
+            url = request.route_url('show_page', name=page_name)
+            return redirect(request, url)
+    return dict(form_value=request.params, form_error=error)
+
+
+def show_page_view(request):
+    name = request.matchdict.get('name')
+    page = request.context.get_page(name)
+    return dict(page=page)
+
+
+def update_page_view(request):
+    schema = forms.UpdatePageSchema()
+
+    error = None
+    try:
+        value = schema.to_python(request.params)
+    except Invalid, e:
+        error = e
+    else:
+        page_name = value['name']
+        content = value['content']
+        try:
+            request.context.update_page(page_name, content)
+        except exc.PageNotFound:
+            request.session.flash('Page not found')
+        else:
+            request.session.flash('Successfully updated')
+            url = request.route_url('show_page', name=page_name)
+            return redirect(request, url)
+    return dict(form_value=request.params, form_error=error)
+# -*- coding: utf-8 -*-
+
+"""
+The Wiki module originally written by ianb.
+
+http://docutils.sourceforge.net/sandbox/ianb/wiki/Wiki.py
+"""
+
+########################################
+## reST-specific stuff
+########################################
+
+import re
+import docutils.core
+from docutils.nodes import SparseNodeVisitor
+from docutils.transforms import Transform
+from docutils.readers.standalone import Reader as StandaloneReader
+
+__all__ = ['substitute_links', 'convert_text']
+
+wiki_link_re = re.compile(r'(<a [^>]* href=")!(.*?)("[^>]*>)(.*?)(</a>)',
+                           re.I+re.S)
+
+
+def substitute_links(text, exists_func):
+    def repl(match):
+        if exists_func(match.group(2)):
+            return match.group(1) + match.group(2) + match.group(3) + \
+                match.group(4) + match.group(5)
+        else:
+            return '<span class="nowiki">%s%s%s%s?%s</span>' \
+                   % (match.group(4), match.group(1), match.group(2),
+                      match.group(3), match.group(5))
+
+    return wiki_link_re.sub(repl, ' %s ' % text)
+
+
+def convert_text(text):
+    return clean_html(docutils.core.publish_string(
+        source=text,
+        reader=WikiReader(),
+        parser_name='restructuredtext',
+        writer_name='html')).decode('utf-8')
+
+def clean_html(html):
+    start = html.find('<body>') + 6
+    end = html.find('</body>')
+    return html[start:end]
+
+
+class WikiLinkResolver(SparseNodeVisitor):
+
+    def visit_reference(self, node):
+        if node.resolved or not node.hasattr('refname'):
+            return
+        refname = node['refname']
+        node.resolved = 1
+        node['class'] = 'wiki'
+        # I put a ! here to distinguish Wiki links from other
+        # links -- Wiki links have to be fixed up at view time,
+        # to distinguish between dangling and resolved Wiki
+        # links.
+        node['refuri'] = '!' + refname
+        del node['refname']
+
+
+class WikiLink(Transform):
+
+    default_priority = 800
+
+    def apply(self):
+        visitor = WikiLinkResolver(self.document)
+        self.document.walk(visitor)
+
+
+class WikiReader(StandaloneReader):
+
+    supported = StandaloneReader.supported + ('wiki',)
+
+    def get_transforms(self):
+        return StandaloneReader.get_transforms(self) + [WikiLink]
 README = open(os.path.join(here, 'README.txt')).read()
 CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
 
-requires = ['pyramid', 'pyramid_debugtoolbar']
+requires = [
+    'pyramid',
+    'pyramid_debugtoolbar',
+    'pyramid_tm',
+    'pyramid_jinja2',
+    'sqlahelper',
+    'formencode',
+    'docutils',
+    ]
 
 setup(name='HgWiki',
       version='0.0',
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.