Wiki: Base wiki code

- Updates snudown dep
This commit is contained in:
Andre D
2012-08-24 11:48:43 -05:00
committed by Neil Williams
parent ebab524e5d
commit 5fe4e997d8
46 changed files with 2070 additions and 69 deletions

View File

@@ -35,8 +35,6 @@ log_start = true
amqp_logging = false
# emergency measures: makes the site read only
read_only_mode = false
# global switch for wiki write permissions
allow_wiki_editing = true
# a modified read only mode used for cache shown during heavy load 503s
heavy_load_mode = false
# directory to write cProfile stats dumps to (disabled if not set)
@@ -424,9 +422,6 @@ ADMIN_COOKIE_MAX_IDLE = 900
# the maximum life of an otp cookie
OTP_COOKIE_TTL = 604800
# min amount of karma to edit
WIKI_KARMA = 100
# time in days
MODWINDOW = 2
HOT_PAGE_AGE = 1000
@@ -460,6 +455,8 @@ agents =
sr_banned_quota = 10000
sr_moderator_quota = 10000
sr_contributor_quota = 10000
sr_wikibanned_quota = 10000
sr_wikicontributor_quota = 10000
sr_quota_time = 7200
# -- email --
@@ -477,9 +474,25 @@ feedback_email = reddit@gmail.com
# Special case sensitive domains
case_sensitive_domains = i.imgur.com, youtube.com
# Number of days to keep recent wiki revisions for
wiki_keep_recent_days = 7
# Max number of bytes for wiki pages
wiki_max_page_length_bytes = 262144
# Max wiki page name length
wiki_max_page_name_length = 128
# Max number of separators in a wiki page name
wiki_max_page_separators = 3
# Disable wiki editing and viewing for everyone except admins
wiki_disabled = false
# Location (directory) for temp files for diff3 merging
# Empty will use python default for temp files
diff3_temp_location = /dev/shm
# Pro tip: Use /dev/shm for in-memory diff3
diff3_temp_location =
[server:main]
use = egg:Paste#http

View File

@@ -73,7 +73,10 @@ def error_mapper(code, message, environ, global_conf=None, **kw):
exception = environ.get('r2.controller.exception')
if exception:
d['explanation'] = exception.explanation
error_data = getattr(exception, 'error_data', None)
if error_data:
environ['extra_error_data'] = error_data
if environ.get('REDDIT_CNAME'):
d['cnameframe'] = 1
if environ.get('REDDIT_NAME'):

View File

@@ -185,7 +185,25 @@ def make_map():
mc('/:action', controller='embed',
requirements=dict(action="help|blog|faq"))
mc('/help/*anything', controller='embed', action='help')
mc('/wiki/create/*page', controller='wiki', action='wiki_create')
mc('/wiki/edit/*page', controller='wiki', action='wiki_revise')
mc('/wiki/revisions/*page', controller='wiki', action='wiki_revisions')
mc('/wiki/settings/*page', controller='wiki', action='wiki_settings')
mc('/wiki/discussions/*page', controller='wiki', action='wiki_discussions')
mc('/wiki/revisions', controller='wiki', action='wiki_recent')
mc('/wiki/pages', controller='wiki', action='wiki_listing')
mc('/wiki/api/edit/*page', controller='wikiapi', action='wiki_edit')
mc('/wiki/api/hide/:revision/*page', controller='wikiapi', action='wiki_revision_hide')
mc('/wiki/api/revert/:revision/*page', controller='wikiapi', action='wiki_revision_revert')
mc('/wiki/api/alloweditor/:act/:username/*page', controller='wikiapi', action='wiki_allow_editor')
mc('/wiki/*page', controller='wiki', action='wiki_page')
mc('/wiki/', controller='wiki', action='wiki_page')
mc('/w/*page', controller='wiki', action='wiki_redirect')
mc('/goto', controller='toolbar', action='goto')
mc('/tb/:id', controller='toolbar', action='tb')
mc('/toolbar/:action', controller='toolbar',

View File

@@ -69,6 +69,9 @@ def load_controllers():
from promotecontroller import PromoteController
from mediaembed import MediaembedController
from mediaembed import AdController
from wiki import WikiController
from wiki import WikiApiController
from querycontroller import QueryController

View File

@@ -31,6 +31,8 @@ from pylons.i18n import _
import random as rand
from r2.lib.filters import safemarkdown, unsafe
import json
try:
# place all r2 specific imports in here. If there is a code error, it'll get caught and
# the stack trace won't be presented to the user in production
@@ -165,10 +167,8 @@ class ErrorController(RedditController):
c.response.content = str(code)
return c.response
elif c.render_style == "api":
if 'usable_error_content' in request.environ:
c.response.content = request.environ['usable_error_content']
else:
c.response.content = "{\"error\": %s}" % code
data = request.environ.get('extra_error_data', {'error': code})
c.response.content = json.dumps(data)
return c.response
elif takedown and code == 404:
link = Link._by_fullname(takedown)

View File

@@ -20,8 +20,9 @@
# Inc. All Rights Reserved.
###############################################################################
from paste.httpexceptions import HTTPForbidden
from paste.httpexceptions import HTTPForbidden, HTTPError
from r2.lib.utils import Storage, tup
from pylons import request
from pylons.i18n import _
from copy import copy
@@ -98,6 +99,7 @@ error_list = dict((
('OAUTH2_INVALID_SCOPE', _('invalid scope requested')),
('OAUTH2_ACCESS_DENIED', _('access denied by the user')),
('CONFIRM', _("please confirm the form")),
('CONFLICT', _("conflict error while saving")),
('NO_API', _('cannot perform this action via the API')),
('DOMAIN_BANNED', _('%(domain)s is not allowed on reddit: %(reason)s')),
('NO_OTP_SECRET', _('you must enable two-factor authentication')),
@@ -110,12 +112,13 @@ errors = Storage([(e, e) for e in error_list.keys()])
class Error(object):
def __init__(self, name, i18n_message, msg_params, field = None):
def __init__(self, name, i18n_message, msg_params, field=None, code=None):
self.name = name
self.i18n_message = i18n_message
self.msg_params = msg_params
# list of fields in the original form that caused the error
self.fields = tup(field) if field else []
self.code = code
@property
def message(self):
@@ -151,10 +154,10 @@ class ErrorSet(object):
def __len__(self):
return len(self.errors)
def add(self, error_name, msg_params = {}, field = None):
msg = error_list[error_name]
def add(self, error_name, msg_params={}, field=None, code=None):
msg = error_list.get(error_name)
for field_name in tup(field):
e = Error(error_name, msg, msg_params, field = field_name)
e = Error(error_name, msg, msg_params, field=field_name, code=code)
self.errors[(error_name, field_name)] = e
def remove(self, pair):
@@ -163,6 +166,13 @@ class ErrorSet(object):
if self.errors.has_key(pair):
del self.errors[pair]
class WikiError(HTTPError):
def __init__(self, code, reason=None, **data):
self.code = code
data['reason'] = self.explanation = reason or 'UNKNOWN_ERROR'
self.error_data = data
HTTPError.__init__(self)
class ForbiddenError(HTTPForbidden):
def __init__(self, error):
HTTPForbidden.__init__(self)

View File

@@ -871,7 +871,9 @@ class FrontController(RedditController):
captcha = Captcha() if c.user.needs_captcha() else None
sr_names = (Subreddit.submit_sr_names(c.user) or
Subreddit.submit_sr_names(None))
never_show_self = request.get.get('no_self')
return FormPage(_("submit"),
show_sidebar = True,
page_classes=['submit-page'],
@@ -882,6 +884,7 @@ class FrontController(RedditController):
subreddits = sr_names,
captcha=captcha,
resubmit=resubmit,
never_show_self = never_show_self,
then = then)).render()
def GET_frame(self):
@@ -1191,7 +1194,7 @@ class FormsController(RedditController):
returns their user name"""
c.response_content_type = 'text/plain'
if c.user_is_loggedin:
perm = str(g.allow_wiki_editing and c.user.can_wiki())
perm = str(c.user.can_wiki())
c.response.content = c.user.name + "," + perm
else:
c.response.content = ''

View File

@@ -603,7 +603,9 @@ class MinimalController(BaseController):
ratelimit_agents()
c.allow_loggedin_cache = False
c.show_wiki_actions = False
# the domain has to be set before Cookies get initialized
set_subreddit()
c.errors = ErrorSet()

View File

@@ -85,7 +85,7 @@ class Validator(object):
self.post, self.get, self.url, self.docs = post, get, url, docs
self.has_errors = False
def set_error(self, error, msg_params = {}, field = False):
def set_error(self, error, msg_params={}, field=False, code=None):
"""
Adds the provided error to c.errors and flags that it is come
from the validator's param
@@ -93,7 +93,7 @@ class Validator(object):
if field is False:
field = self.param
c.errors.add(error, msg_params = msg_params, field = field)
c.errors.add(error, msg_params=msg_params, field=field, code=code)
self.has_errors = True
def param_docs(self):
@@ -161,6 +161,8 @@ def set_api_docs(fn, simple_vals, param_vals):
param_info.update(validator.param_docs())
doc['parameters'] = param_info
make_validated_kw = _make_validated_kw
def validate(*simple_vals, **param_vals):
def val(fn):
@utils.wraps_api(fn)

View File

@@ -0,0 +1,274 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of
# the Original Code is reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2012 reddit
# Inc. All Rights Reserved.
###############################################################################
from os.path import normpath
import datetime
from pylons.controllers.util import redirect_to
from pylons import c, g, request
from r2.models.wiki import WikiPage, WikiRevision
from r2.controllers.validator import Validator, validate, make_validated_kw
from r2.lib.db import tdb_cassandra
MAX_PAGE_NAME_LENGTH = g.wiki_max_page_name_length
MAX_SEPARATORS = g.wiki_max_page_separators
def wiki_validate(*simple_vals, **param_vals):
def val(fn):
def newfn(self, *a, **env):
kw = make_validated_kw(fn, simple_vals, param_vals, env)
for e in c.errors:
e = c.errors[e]
if e.code:
self.handle_error(e.code, e.name)
return fn(self, *a, **kw)
return newfn
return val
def this_may_revise(page=None):
if not c.user_is_loggedin:
return False
if c.user_is_admin:
return True
return may_revise(c.site, c.user, page)
def this_may_view(page):
user = c.user if c.user_is_loggedin else None
return may_view(c.site, user, page)
def may_revise(sr, user, page=None):
if sr.is_moderator(user):
# Mods may always contribute
return True
if page and page.restricted and not page.special:
# People may not contribute to restricted pages
# (Except for special pages)
return False
if sr.is_wikibanned(user):
# Users who are wiki banned in the subreddit may not contribute
return False
if page and not may_view(sr, user, page):
# Users who are not allowed to view the page may not contribute to the page
return False
if not user.can_wiki():
# Global wiki contributute ban
return False
if page and page.has_editor(user.name):
# If the user is an editor on the page, they may edit
return True
if not sr.can_submit(user):
# If the user can not submit to the subreddit
# They should not be able to contribute
return False
if page and page.special:
# If this is a special page
# (and the user is not a mod or page editor)
# They should not be allowed to revise
return False
if page and page.permlevel > 0:
# If the page is beyond "anyone may contribute"
# A normal user should not be allowed to revise
return False
if sr.is_wikicontributor(user):
# If the user is a wiki contributor, they may revise
return True
karma = max(user.karma('link', sr), user.karma('comment', sr))
if karma < sr.wiki_edit_karma:
# If the user has too few karma, they should not contribute
return False
age = (datetime.datetime.now(g.tz) - user._date).days
if age < sr.wiki_edit_age:
# If they user's account is too young
# They should not contribute
return False
# Otherwise, allow them to contribute
return True
def may_view(sr, user, page):
# User being None means not logged in
mod = sr.is_moderator(user) if user else False
if mod:
# Mods may always view
return True
if page.special:
# Special pages may always be viewed
# (Permission level ignored)
return True
level = page.permlevel
if level < 2:
# Everyone may view in levels below 2
return True
if level == 2:
# Only mods may view in level 2
return mod
# In any other obscure level,
# (This should not happen but just in case)
# nobody may view.
return False
def normalize_page(page):
# Case insensitive page names
page = page.lower()
# Normalize path
page = normpath(page)
# Chop off initial "/", just in case it exists
page = page.lstrip('/')
return page
class AbortWikiError(Exception):
pass
class VWikiPage(Validator):
def __init__(self, param, required=True, restricted=True, modonly=False, **kw):
self.restricted = restricted
self.modonly = modonly
self.required = required
Validator.__init__(self, param, **kw)
def run(self, page):
if not page:
# If no page is specified, give the index page
page = "index"
try:
page = str(page)
except UnicodeEncodeError:
return self.set_error('INVALID_PAGE_NAME', code=400)
if ' ' in page:
new_name = page.replace(' ', '_')
url = '%s/%s' % (c.wiki_base_url, new_name)
redirect_to(url)
page = normalize_page(page)
c.page = page
if (not c.is_wiki_mod) and self.modonly:
return self.set_error('MOD_REQUIRED', code=403)
try:
wp = self.validpage(page)
except AbortWikiError:
return
# TODO: MAKE NOT REQUIRED
c.page_obj = wp
return wp
def validpage(self, page):
try:
wp = WikiPage.get(c.site, page)
if self.restricted and wp.restricted:
if not wp.special:
self.set_error('RESTRICTED_PAGE', code=403)
raise AbortWikiError
if not this_may_view(wp):
self.set_error('MAY_NOT_VIEW', code=403)
raise AbortWikiError
return wp
except tdb_cassandra.NotFound:
if self.required:
self.set_error('PAGE_NOT_FOUND', code=404)
raise AbortWikiError
return None
def validversion(self, version, pageid=None):
if not version:
return
try:
r = WikiRevision.get(version, pageid)
if r.is_hidden and not c.is_wiki_mod:
self.set_error('HIDDEN_REVISION', code=403)
raise AbortWikiError
return r
except (tdb_cassandra.NotFound, ValueError):
self.set_error('INVALID_REVISION', code=404)
raise AbortWikiError
class VWikiPageAndVersion(VWikiPage):
def run(self, page, *versions):
wp = VWikiPage.run(self, page)
validated = []
for v in versions:
try:
validated += [self.validversion(v, wp._id) if v and wp else None]
except AbortWikiError:
return
return tuple([wp] + validated)
class VWikiPageRevise(VWikiPage):
def run(self, page, previous=None):
wp = VWikiPage.run(self, page)
if not wp:
return self.set_error('INVALID_PAGE', code=404)
if not this_may_revise(wp):
return self.set_error('MAY_NOT_REVISE', code=403)
if previous:
try:
prev = self.validversion(previous, wp._id)
except AbortWikiError:
return
return (wp, prev)
return (wp, None)
class VWikiPageCreate(Validator):
def run(self, page):
page = normalize_page(page)
if c.is_wiki_mod and WikiPage.is_special(page):
c.error = {'reason': 'PAGE_CREATED_ELSEWHERE'}
elif page.count('/') > MAX_SEPARATORS:
c.error = {'reason': 'PAGE_NAME_MAX_SEPARATORS', 'max_separators': MAX_SEPERATORS}
elif len(page) > MAX_PAGE_NAME_LENGTH:
c.error = {'reason': 'PAGE_NAME_LENGTH', 'max_length': MAX_PAGE_NAME_LENGTH}
else:
try:
WikiPage.get(c.site, page)
c.error = {'reason': 'PAGE_EXISTS'}
except tdb_cassandra.NotFound:
pass
return this_may_revise()

299
r2/r2/controllers/wiki.py Normal file
View File

@@ -0,0 +1,299 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
from pylons import request, g, c, Response
from pylons.controllers.util import redirect_to
from reddit_base import RedditController
from r2.lib.utils import url_links
from reddit_base import paginated_listing
from r2.models.wiki import (WikiPage, WikiRevision, ContentLengthError,
modactions)
from r2.models.subreddit import Subreddit
from r2.models.modaction import ModAction
from r2.models.builder import WikiRevisionBuilder, WikiRecentRevisionBuilder
from r2.lib.template_helpers import join_urls
from r2.controllers.validator import VMarkdown
from r2.controllers.validator.wiki import (VWikiPage, VWikiPageAndVersion,
VWikiPageRevise, VWikiPageCreate,
this_may_view, wiki_validate)
from r2.lib.pages.wiki import (WikiPageView, WikiNotFound, WikiRevisions,
WikiEdit, WikiSettings, WikiRecent,
WikiListing, WikiDiscussions)
from r2.config.extensions import set_extension
from r2.lib.template_helpers import add_sr
from r2.lib.db import tdb_cassandra
from r2.controllers.errors import errors
from r2.models.listing import WikiRevisionListing
from r2.lib.pages.things import default_thing_wrapper
from r2.lib.pages import BoringPage
from reddit_base import base_listing
from r2.models import IDBuilder, LinkListing, DefaultSR
from validator.validator import VInt, VExistingUname, VRatelimit
from r2.lib.merge import ConflictException, make_htmldiff
from pylons.i18n import _
from r2.lib.pages import PaneStack
from r2.lib.utils import timesince
from r2.lib.base import abort
from r2.controllers.errors import WikiError
import json
page_descriptions = {'config/stylesheet':_("This page is the subreddit stylesheet, changes here apply to the subreddit css"),
'config/sidebar':_("The contents of this page appear on the subreddit sidebar")}
class WikiController(RedditController):
allow_stylesheets = True
@wiki_validate(pv=VWikiPageAndVersion(('page', 'v', 'v2'), required=False,
restricted=False))
def GET_wiki_page(self, pv):
page, version, version2 = pv
message = None
if not page:
return self.GET_wiki_create(page=c.page, view=True)
if version:
edit_by = version.author_name()
edit_date = version.date
else:
edit_by = page.author_name()
edit_date = page._get('last_edit_date')
diffcontent = None
if not version:
content = page.content
if c.is_wiki_mod and page.name in page_descriptions:
message = page_descriptions[page.name]
else:
message = _("viewing revision from %s") % timesince(version.date)
if version2:
t1 = timesince(version.date)
t2 = timesince(version2.date)
timestamp1 = _("%s ago") % t1
timestamp2 = _("%s ago") % t2
message = _("comparing revisions from %(date_1)s and %(date_2)s") \
% {'date_1': t1, 'date_2': t2}
diffcontent = make_htmldiff(version.content, version2.content, timestamp1, timestamp2)
content = version2.content
else:
message = _("viewing revision from %s ago") % timesince(version.date)
content = version.content
return WikiPageView(content, alert=message, v=version, diff=diffcontent,
edit_by=edit_by, edit_date=edit_date).render()
@paginated_listing(max_page_size=100, backend='cassandra')
@wiki_validate(page=VWikiPage(('page'), restricted=False))
def GET_wiki_revisions(self, num, after, reverse, count, page):
revisions = page.get_revisions()
builder = WikiRevisionBuilder(revisions, num=num, reverse=reverse, count=count, after=after, skip=not c.is_wiki_mod, wrap=default_thing_wrapper())
listing = WikiRevisionListing(builder).listing()
return WikiRevisions(listing).render()
@wiki_validate(may_create=VWikiPageCreate('page'))
def GET_wiki_create(self, may_create, page, view=False):
api = c.extension == 'json'
if c.error and c.error['reason'] == 'PAGE_EXISTS':
return self.redirect(join_urls(c.wiki_base_url, page))
elif not may_create or api:
if may_create and c.error:
self.handle_error(403, **c.error)
else:
self.handle_error(404, 'PAGE_NOT_FOUND', may_create=may_create)
elif c.error:
error = ''
if c.error['reason'] == 'PAGE_NAME_LENGTH':
error = _("this wiki cannot handle page names of that magnitude! please select a page name shorter than %d characters") % c.error['max_length']
elif c.error['reason'] == 'PAGE_CREATED_ELSEWHERE':
error = _("this page is a special page, please go into the subreddit settings and save the field once to create this special page")
elif c.error['reason'] == 'PAGE_NAME_MAX_SEPERATORS':
error = _('a max of %d separators "/" are allowed in a wiki page name.') % c.error['max_separator']
return BoringPage(_("Wiki error"), infotext=error).render()
elif view:
return WikiNotFound().render()
elif may_create:
WikiPage.create(c.site, page)
url = join_urls(c.wiki_base_url, '/edit/', page)
return self.redirect(url)
@wiki_validate(page=VWikiPageRevise('page', restricted=True))
def GET_wiki_revise(self, page, message=None, **kw):
page = page[0]
previous = kw.get('previous', page._get('revision'))
content = kw.get('content', page.content)
if not message and page.name in page_descriptions:
message = page_descriptions[page.name]
return WikiEdit(content, previous, alert=message).render()
@paginated_listing(max_page_size=100, backend='cassandra')
def GET_wiki_recent(self, num, after, reverse, count):
revisions = WikiRevision.get_recent(c.site)
builder = WikiRecentRevisionBuilder(revisions, num=num, count=count,
reverse=reverse, after=after,
wrap=default_thing_wrapper(),
skip=not c.is_wiki_mod)
listing = WikiRevisionListing(builder).listing()
return WikiRecent(listing).render()
def GET_wiki_listing(self):
def check_hidden(page):
g.log.debug("Got here %s" % str(this_may_view(page)))
return this_may_view(page)
pages = WikiPage.get_listing(c.site, filter_check=check_hidden)
return WikiListing(pages).render()
def GET_wiki_redirect(self, page):
return redirect_to(str("%s/%s" % (c.wiki_base_url, page)), _code=301)
@base_listing
@wiki_validate(page=VWikiPage('page', restricted=True))
def GET_wiki_discussions(self, page, num, after, reverse, count):
page_url = add_sr("%s/%s" % (c.wiki_base_url, page.name))
links = url_links(page_url)
builder = IDBuilder([ link._fullname for link in links ],
num = num, after = after, reverse = reverse,
count = count, skip = False)
listing = LinkListing(builder).listing()
return WikiDiscussions(listing).render()
@wiki_validate(page=VWikiPage('page', restricted=True, modonly=True))
def GET_wiki_settings(self, page):
settings = {'permlevel': page._get('permlevel', 0)}
mayedit = page.get_editors()
return WikiSettings(settings, mayedit, show_settings=not page.special).render()
@wiki_validate(page=VWikiPage('page', restricted=True, modonly=True),\
permlevel=VInt('permlevel'))
def POST_wiki_settings(self, page, permlevel):
oldpermlevel = page.permlevel
try:
page.change_permlevel(permlevel)
except ValueError:
self.handle_error(403, 'INVALID_PERMLEVEL')
description = 'Page: %s, Changed from %s to %s' % (page.name, oldpermlevel, permlevel)
ModAction.create(c.site, c.user, 'wikipermlevel', description=description)
return self.GET_wiki_settings(page=page.name)
def handle_error(self, code, error=None, **data):
abort(WikiError(code, error, **data))
def pre(self):
RedditController.pre(self)
if g.wiki_disabled and not c.user_is_admin:
self.handle_error(403, 'WIKI_DOWN')
if not c.site._should_wiki:
self.handle_error(404, 'NOT_WIKIABLE') # /r/mod for an example
frontpage = isinstance(c.site, DefaultSR)
c.wiki_base_url = '/wiki' if frontpage else '/r/%s/wiki' % c.site.name
c.wiki_id = g.default_sr if frontpage else c.site.name
c.page = None
c.show_wiki_actions = True
self.editconflict = False
c.is_wiki_mod = (c.user_is_admin or c.site.is_moderator(c.user)) if c.user_is_loggedin else False
c.wikidisabled = False
mode = c.site.wikimode
if not mode or mode == 'disabled':
if not c.is_wiki_mod:
self.handle_error(403, 'WIKI_DISABLED')
else:
c.wikidisabled = True
class WikiApiController(WikiController):
@wiki_validate(pageandprevious=VWikiPageRevise(('page', 'previous'), restricted=True),
content=VMarkdown(('content')))
def POST_wiki_edit(self, pageandprevious, content):
page, previous = pageandprevious
previous = previous._id if previous else None
try:
if page.name == 'config/stylesheet':
report, parsed = c.site.parse_css(content, verify=False)
if report is None: # g.css_killswitch
self.handle_error(403, 'STYLESHEET_EDIT_DENIED')
if report.errors:
error_items = [x.message for x in sorted(report.errors)]
self.handle_error(415, 'SPECIAL_ERRORS', special_errors=error_items)
c.site.change_css(content, parsed, previous, reason=request.POST['reason'])
else:
try:
page.revise(content, previous, c.user.name, reason=request.POST['reason'])
except ContentLengthError as e:
self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length = e.max_length)
if page.special or c.is_wiki_mod:
description = modactions.get(page.name, 'Page %s edited' % page.name)
ModAction.create(c.site, c.user, 'wikirevise', details=description)
except ConflictException as e:
self.handle_error(409, 'EDIT_CONFLICT', newcontent=e.new, newrevision=page.revision, diffcontent=e.htmldiff)
return json.dumps({})
@wiki_validate(page=VWikiPage('page'), user=VExistingUname('username'))
def POST_wiki_allow_editor(self, act, page, user):
if not c.is_wiki_mod:
self.handle_error(403, 'MOD_REQUIRED')
if act == 'del':
page.remove_editor(c.username)
else:
if not user:
self.handle_error(404, 'UNKOWN_USER')
page.add_editor(user.name)
return json.dumps({})
@wiki_validate(pv=VWikiPageAndVersion(('page', 'revision')))
def POST_wiki_revision_hide(self, pv, page, revision):
if not c.is_wiki_mod:
self.handle_error(403, 'MOD_REQUIRED')
page, revision = pv
return json.dumps({'status': revision.toggle_hide()})
@wiki_validate(pv=VWikiPageAndVersion(('page', 'revision')))
def POST_wiki_revision_revert(self, pv, page, revision):
if not c.is_wiki_mod:
self.handle_error(403, 'MOD_REQUIRED')
page, revision = pv
content = revision.content
author = revision._get('author')
reason = 'reverted back %s' % timesince(revision.date)
if page.name == 'config/stylesheet':
report, parsed = c.site.parse_css(content)
if report.errors:
self.handle_error(403, 'INVALID_CSS')
c.site.change_css(content, parsed, prev=None, reason=reason, force=True)
else:
try:
page.revise(content, author=author, reason=reason, force=True)
except ContentLengthError as e:
self.handle_error(403, 'CONTENT_LENGTH_ERROR', e.max_length)
return json.dumps({})
def pre(self):
WikiController.pre(self)
c.render_style = 'api'
set_extension(request.environ, 'json')

View File

@@ -82,7 +82,6 @@ class Globals(object):
'MIN_RATE_LIMIT_COMMENT_KARMA',
'VOTE_AGE_LIMIT',
'REPLY_AGE_LIMIT',
'WIKI_KARMA',
'HOT_PAGE_AGE',
'MODWINDOW',
'RATELIMIT',
@@ -103,9 +102,15 @@ class Globals(object):
'bcrypt_work_factor',
'cassandra_pool_size',
'sr_banned_quota',
'sr_wikibanned_quota',
'sr_wikicontributor_quota',
'sr_moderator_quota',
'sr_contributor_quota',
'sr_quota_time',
'wiki_keep_recent_days',
'wiki_max_page_length_bytes',
'wiki_max_page_name_length',
'wiki_max_page_separators',
],
ConfigValue.float: [
@@ -134,7 +139,7 @@ class Globals(object):
'disable_ratelimit',
'amqp_logging',
'read_only_mode',
'allow_wiki_editing',
'wiki_disabled',
'heavy_load_mode',
's3_media_direct',
'disable_captcha',

View File

@@ -177,6 +177,17 @@ class ValidationError(Exception):
obj = str(self.obj) if hasattr(self,'obj') else ''
return "ValidationError%s: %s (%s)" % (line, self.message, obj)
def legacy_s3_url(url, site):
if isinstance(url, int): # legacy url, needs to be generated
bucket = g.s3_old_thumb_bucket
baseurl = "http://%s" % (bucket)
if g.s3_media_direct:
baseurl = "http://%s/%s" % (s3_direct_url, bucket)
url = "%s/%s_%d.png"\
% (baseurl, site._fullname, url)
url = s3_https_if_secure(url)
return url
# local urls should be in the static directory
local_urls = re.compile(r'\A/static/[a-z./-]+\Z')
# substitutable urls will be css-valid labels surrounded by "%%"
@@ -211,14 +222,7 @@ def valid_url(prop,value,report):
# the label -> image number lookup is stored on the subreddit
if c.site.images.has_key(name):
url = c.site.images[name]
if isinstance(url, int): # legacy url, needs to be generated
bucket = g.s3_old_thumb_bucket
baseurl = "http://%s" % (bucket)
if g.s3_media_direct:
baseurl = "http://%s/%s" % (s3_direct_url, bucket)
url = "%s/%s_%d.png"\
% (baseurl, c.site._fullname, url)
url = s3_https_if_secure(url)
url = legacy_s3_url(url, c.site)
value._setCssText("url(%s)"%url)
else:
# unknown image label -> error

View File

@@ -42,6 +42,8 @@ SC_ON = "<!-- SC_ON -->"
MD_START = '<div class="md">'
MD_END = '</div>'
WIKI_MD_START = '<div class="md wiki">'
WIKI_MD_END = '</div>'
def python_websafe(text):
return text.replace('&', "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
@@ -175,17 +177,22 @@ class SouptestSaxHandler(ContentHandler):
markdown_ok_tags = {
'div': ('class'),
'a': set(('href', 'title', 'target', 'nofollow')),
'table': ("align", ),
'th': ("align", ),
'td': ("align", ),
}
markdown_boring_tags = ('p', 'em', 'strong', 'br', 'ol', 'ul', 'hr', 'li',
'pre', 'code', 'blockquote', 'center',
'tbody', 'thead', 'tr', 'sup', 'del',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',)
'sup', 'del', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',)
markdown_user_tags = ('table', 'th', 'tr', 'td', 'tbody', 'img',
'tbody', 'thead', 'tr', 'tfoot', 'caption')
for bt in markdown_boring_tags:
markdown_ok_tags[bt] = ()
for bt in markdown_user_tags:
markdown_ok_tags[bt] = ('colspan', 'rowspan', 'cellspacing', 'cellpadding', 'align', 'scope')
markdown_xhtml_dtd_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'contrib/dtds/xhtml.dtd')
@@ -229,6 +236,33 @@ def safemarkdown(text, nofollow=False, wrap=True, **kwargs):
else:
return SC_OFF + text + SC_ON
def wikimarkdown(text):
from r2.lib.cssfilter import legacy_s3_url
def img_swap(tag):
name = tag.get('src')
if c.site.images.has_key(name):
url = c.site.images[name]
url = legacy_s3_url(url, c.site)
tag['src'] = url
else:
tag.extract()
nofollow = True
target = None
text = snudown.markdown(_force_utf8(text), nofollow, target,
renderer=snudown.RENDERER_WIKI, enable_toc=True)
# TODO: We should test how much of a load this adds to the app
soup = BeautifulSoup(text)
images = soup.findAll('img')
if images:
[img_swap(image) for image in images]
text = str(soup)
return SC_OFF + WIKI_MD_START + text + WIKI_MD_END + SC_ON
def keep_space(text):
text = websafe(text)

View File

@@ -282,6 +282,7 @@ module["reddit"] = LocalizedModule("reddit.js",
"analytics.js",
"flair.js",
"interestbar.js",
"wiki.js",
"reddit.js",
"apps.js",
)

View File

@@ -138,6 +138,12 @@ menu = MenuHandler(hot = _('hot'),
log = _("moderation log"),
modqueue = _("moderation queue"),
unmoderated = _("unmoderated links"),
wikibanned = _("ban wiki contributors"),
wikicontributors = _("add wiki contributors"),
wikirecentrevisions = _("recent wiki revisions"),
wikipageslist = _("wiki page list"),
popular = _("popular"),
create = _("create"),

View File

@@ -1,3 +1,25 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of
# the Original Code is reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2012 reddit
# Inc. All Rights Reserved.
###############################################################################
import subprocess
import tempfile
import difflib
@@ -20,7 +42,7 @@ def make_htmldiff(a, b, adesc, bdesc):
fromdesc=adesc,
todesc=bdesc)
def threeWayMerge(original, a, b):
def threewaymerge(original, a, b):
try:
temp_dir = g.diff3_temp_location if g.diff3_temp_location else None
data = [a, original, b]
@@ -49,8 +71,8 @@ if __name__ == "__main__":
a = "Hello people of the human rance\n\nHow are you today"
b = "Hello people of the human race\n\nHow are you tday"
print threeWayMerge(original, a, b)
print threewaymerge(original, a, b)
g.diff3_temp_location = '/dev/shm'
print threeWayMerge(original, a, b)
print threewaymerge(original, a, b)

View File

@@ -191,7 +191,30 @@ class Reddit(Templated):
self._content = content
self.toolbars = self.build_toolbars()
def wiki_actions_menu(self, moderator=False):
buttons = []
buttons.append(NamedButton("wikirecentrevisions",
css_class="wikiaction-revisions",
dest="revisions"))
buttons.append(NamedButton("wikipageslist",
css_class="wikiaction-pages",
dest="pages"))
if moderator:
buttons += [NamedButton('wikibanned', css_class = 'reddit-ban'),
NamedButton('wikicontributors', css_class = 'reddit-contributors')]
return SideContentBox(_('wiki tools'),
[NavMenu(buttons,
type="flat_vert",
base_path="/wiki/",
css_class="icon-menu",
separator="")],
_id="wikiactions",
collapsible=True)
def sr_admin_menu(self):
buttons = []
is_single_subreddit = not isinstance(c.site, (ModSR, MultiReddit))
@@ -263,7 +286,6 @@ class Reddit(Templated):
if isinstance(c.site, (MultiReddit, ModSR)) and c.user_is_loggedin:
srs = Subreddit._byID(c.site.sr_ids,data=True,
return_dict=False)
if c.user_is_admin or c.site.is_moderator(c.user):
ps.append(self.sr_admin_menu())
@@ -273,12 +295,17 @@ class Reddit(Templated):
# don't show the subreddit info bar on cnames unless the option is set
if not isinstance(c.site, FakeSubreddit) and (not c.cname or c.site.show_cname_sidebar):
ps.append(SubredditInfoBar())
if c.user_is_loggedin and (c.user_is_admin or
c.site.is_moderator(c.user)):
moderator = c.user_is_loggedin and (c.user_is_admin or
c.site.is_moderator(c.user))
if c.show_wiki_actions:
ps.append(self.wiki_actions_menu(moderator=moderator))
if moderator:
ps.append(self.sr_admin_menu())
if (c.user.pref_show_adbox or not c.user.gold) and not g.disable_ads:
ps.append(Ads())
no_ads_yet = False
elif c.show_wiki_actions:
ps.append(self.wiki_actions_menu())
user_banned = c.user_is_loggedin and c.site.is_banned(c.user)
if self.submit_box and (c.user_is_loggedin or not g.read_only_mode) and not user_banned:
@@ -388,6 +415,13 @@ class Reddit(Templated):
if c.user_is_loggedin:
main_buttons.append(NamedButton('saved', False))
mod = False
if c.user_is_loggedin:
mod = bool(c.user_is_admin or c.site.is_moderator(c.user))
if c.site.wikimode != 'disabled' or mod:
if not g.wiki_disabled:
main_buttons.append(NavButton('wiki', 'wiki'))
more_buttons = []
if c.user_is_loggedin:
@@ -1888,7 +1922,7 @@ class FrameToolbar(Wrapped):
class NewLink(Templated):
"""Render the link submission form"""
def __init__(self, captcha = None, url = '', title= '', text = '', selftext = '',
subreddits = (), then = 'comments', resubmit=False):
subreddits = (), then = 'comments', resubmit=False, never_show_self=False):
self.show_link = self.show_self = False
@@ -1898,7 +1932,7 @@ class NewLink(Templated):
self.show_link = True
if c.default_sr or c.site.link_type != 'link':
tabs.append(('text', ('text-desc', 'text-field')))
self.show_self = True
self.show_self = not never_show_self
if self.show_self and self.show_link:
all_fields = set(chain(*(parts for (tab, parts) in tabs)))

142
r2/r2/lib/pages/wiki.py Normal file
View File

@@ -0,0 +1,142 @@
from r2.lib.pages.pages import Reddit
from pylons import c
from r2.lib.wrapped import Templated
from r2.lib.menus import PageNameNav
from r2.controllers.validator.wiki import this_may_revise
from r2.lib.filters import wikimarkdown
from pylons.i18n import _
class WikiView(Templated):
def __init__(self, content, edit_by, edit_date, diff=None):
self.page_content = wikimarkdown(content) if content else ''
self.page_content_md = content
self.diff = diff
self.edit_by = edit_by
self.edit_date = edit_date
self.base_url = c.wiki_base_url
self.may_revise = this_may_revise(c.page_obj)
Templated.__init__(self)
class WikiPageListing(Templated):
def __init__(self, pages):
self.pages = pages
self.base_url = c.wiki_base_url
Templated.__init__(self)
class WikiEditPage(Templated):
def __init__(self, page_content, previous):
self.page_content = page_content
self.previous = previous
self.base_url = c.wiki_base_url
Templated.__init__(self)
class WikiPageSettings(Templated):
def __init__(self, settings, mayedit, show_settings=True, **context):
self.permlevel = settings['permlevel']
self.show_settings = show_settings
self.base_url = c.wiki_base_url
self.mayedit = mayedit
Templated.__init__(self)
class WikiPageRevisions(Templated):
def __init__(self, revisions):
self.revisions = revisions
Templated.__init__(self)
class WikiPageDiscussions(Templated):
def __init__(self, listing):
self.listing = listing
Templated.__init__(self)
class WikiBasePage(Templated):
def __init__(self, content, action, pageactions, showtitle=False, description=None, **context):
self.pageactions = pageactions
self.base_url = c.wiki_base_url
self.action = action
self.description = description
if showtitle:
self.title = action[1]
else:
self.title = None
self.content = content
Templated.__init__(self)
class WikiBase(Reddit):
extra_page_classes = ['wiki-page']
def __init__(self, content, actionless=False, alert=None, **context):
pageactions = []
if not actionless and c.page:
pageactions += [(c.page, _("view"), False)]
if this_may_revise(c.page_obj):
pageactions += [('edit', _("edit"), True)]
pageactions += [('revisions/%s' % c.page, _("history"), False)]
pageactions += [('discussions', _("talk"), True)]
if c.is_wiki_mod:
pageactions += [('settings', _("settings"), True)]
action = context.get('wikiaction', (c.page, 'wiki'))
context['title'] = c.site.name
if alert:
context['infotext'] = alert
elif c.wikidisabled:
context['infotext'] = _("this wiki is currently disabled, only mods may interact with this wiki")
context['content'] = WikiBasePage(content, action, pageactions, **context)
Reddit.__init__(self, **context)
class WikiPageView(WikiBase):
def __init__(self, content, diff=None, **context):
if not content and not context.get('alert'):
if this_may_revise(c.page_obj):
context['alert'] = _("this page is empty, edit it to add some content.")
content = WikiView(content, context.get('edit_by'), context.get('edit_date'), diff=diff)
WikiBase.__init__(self, content, **context)
class WikiNotFound(WikiPageView):
def __init__(self, **context):
context['alert'] = _("page %s does not exist in this subreddit") % c.page
context['actionless'] = True
create_link = '%s/create/%s' % (c.wiki_base_url, c.page)
text = _("a page with that name does not exist in this subreddit.\n\n[Create a page called %s](%s)") % (c.page, create_link)
WikiPageView.__init__(self, text, **context)
class WikiEdit(WikiBase):
def __init__(self, content, previous, **context):
content = WikiEditPage(content, previous)
context['wikiaction'] = ('edit', _("editing"))
WikiBase.__init__(self, content, **context)
class WikiSettings(WikiBase):
def __init__(self, settings, mayedit, **context):
content = WikiPageSettings(settings, mayedit, **context)
context['wikiaction'] = ('settings', _("settings"))
WikiBase.__init__(self, content, **context)
class WikiRevisions(WikiBase):
def __init__(self, revisions, **context):
content = WikiPageRevisions(revisions)
context['wikiaction'] = ('revisions/%s' % c.page, _("revisions"))
WikiBase.__init__(self, content, **context)
class WikiRecent(WikiBase):
def __init__(self, revisions, **context):
content = WikiPageRevisions(revisions)
context['wikiaction'] = ('revisions', _("Viewing recent revisions for /r/%s") % c.wiki_id)
WikiBase.__init__(self, content, showtitle=True, **context)
class WikiListing(WikiBase):
def __init__(self, pages, **context):
content = WikiPageListing(pages)
context['wikiaction'] = ('pages', _("Viewing pages for /r/%s") % c.wiki_id)
WikiBase.__init__(self, content, showtitle=True, **context)
class WikiDiscussions(WikiBase):
def __init__(self, listing, **context):
content = WikiPageDiscussions(listing)
context['wikiaction'] = ('discussions', _("discussions"))
description = _("Discussions are site-wide links to this wiki page.<br/>\
Submit a link to this wiki page or see other discussions about this wiki page.")
WikiBase.__init__(self, content, description=description, **context)

View File

@@ -1040,9 +1040,9 @@ def link_duplicates(article):
if getattr(article, 'is_self', False):
return []
return url_links(article.url, is_not = article._fullname)
return url_links(article.url, exclude = article._fullname)
def url_links(url, is_not = None):
def url_links(url, exclude=None):
from r2.models import Link, NotFound
try:
@@ -1051,7 +1051,7 @@ def url_links(url, is_not = None):
links = []
links = [ link for link in links
if link._fullname != is_not ]
if link._fullname != exclude ]
return links
class TimeoutFunctionException(Exception):

View File

@@ -101,7 +101,7 @@ class Account(Thing):
has_subscribed = False,
pref_media = 'subreddit',
share = {},
wiki_override = None,
wiki_override = True,
email = "",
email_verified = False,
ignorereports = False,
@@ -173,12 +173,11 @@ class Account(Thing):
return max(karma, 1) if karma > -1000 else karma
def can_wiki(self):
if self.wiki_override is not None:
return self.wiki_override
else:
return (self.link_karma >= g.WIKI_KARMA and
self.comment_karma >= g.WIKI_KARMA)
if self.wiki_override is None:
# Legacy, None means user may wiki
return True
return self.wiki_override
def jury_betatester(self):
if g.cache.get("jury-killswitch"):
return False

View File

@@ -50,7 +50,9 @@ class ModAction(tdb_cassandra.UuidThing, Printable):
actions = ('banuser', 'unbanuser', 'removelink', 'approvelink',
'removecomment', 'approvecomment', 'addmoderator',
'removemoderator', 'addcontributor', 'removecontributor',
'editsettings', 'editflair', 'distinguish', 'marknsfw')
'editsettings', 'editflair', 'distinguish', 'marknsfw',
'wikibanned', 'wikicontributor', 'wikiunbanned',
'removewikicontributor', 'wikirevise', 'wikipermlevel')
_menu = {'banuser': _('ban user'),
'unbanuser': _('unban user'),
@@ -65,9 +67,19 @@ class ModAction(tdb_cassandra.UuidThing, Printable):
'editsettings': _('edit settings'),
'editflair': _('edit flair'),
'distinguish': _('distinguish'),
'marknsfw': _('mark nsfw')}
'marknsfw': _('mark nsfw'),
'wikibanned': _('ban from wiki'),
'wikiunbanned': _('unban from wiki'),
'wikicontributor': _('add wiki contributor'),
'removewikicontributor': _('remove wiki contributor'),
'wikirevise': _('wiki revise page'),
'wikipermlevel': _('wiki page permlevel')}
_text = {'banuser': _('banned'),
'wikibanned': _('wiki banned'),
'wikiunbanned': _('unbanned from wiki'),
'wikicontributor': _('added wiki contributor'),
'removewikicontributor': _('removed wiki contributor'),
'unbanuser': _('unbanned'),
'removelink': _('removed'),
'approvelink': _('approved'),
@@ -79,6 +91,8 @@ class ModAction(tdb_cassandra.UuidThing, Printable):
'removecontributor': _('removed approved contributor'),
'editsettings': _('edited settings'),
'editflair': _('edited flair'),
'wikirevise': _('edited wiki page'),
'wikipermlevel': _('changed wiki page permission level'),
'distinguish': _('distinguished'),
'marknsfw': _('marked nsfw')}

View File

@@ -69,6 +69,9 @@ class Subreddit(Thing, Printable):
show_cname_sidebar = False,
css_on_cname = True,
domain = None,
wikimode = "disabled",
wiki_edit_karma = 100,
wiki_edit_age = 0,
over_18 = False,
mod_actions = 0,
sponsorship_text = "this reddit is sponsored by",
@@ -200,7 +203,49 @@ class Subreddit(Thing, Printable):
@property
def moderators(self):
return self.moderator_ids()
@property
def stylesheet_contents_user(self):
try:
return WikiPage.get(self, 'config/stylesheet')._get('content','')
except tdb_cassandra.NotFound:
return self._t.get('stylesheet_contents_user')
@property
def prev_stylesheet(self):
try:
return WikiPage.get(self, 'config/stylesheet')._get('revision','')
except tdb_cassandra.NotFound:
return ''
@property
def description(self):
try:
return WikiPage.get(self, 'config/sidebar')._get('content','')
except tdb_cassandra.NotFound:
return self._t.get('description')
@property
def public_description(self):
try:
return WikiPage.get(self, 'config/description')._get('content','')
except tdb_cassandra.NotFound:
return self._t.get('public_description')
@property
def prev_description_id(self):
try:
return WikiPage.get(self, 'config/sidebar')._get('revision','')
except tdb_cassandra.NotFound:
return ''
@property
def prev_public_description_id(self):
try:
return WikiPage.get(self, 'config/description')._get('revision','')
except tdb_cassandra.NotFound:
return ''
@property
def contributors(self):
return self.contributor_ids()
@@ -208,6 +253,18 @@ class Subreddit(Thing, Printable):
@property
def banned(self):
return self.banned_ids()
@property
def wikibanned(self):
return self.wikibanned_ids()
@property
def wikicontributor(self):
return self.wikicontributor_ids()
@property
def _should_wiki(self):
return True
@property
def subscribers(self):
@@ -284,6 +341,31 @@ class Subreddit(Thing, Printable):
return c.user_is_admin or self.is_moderator(user)
else:
return False
def parse_css(self, content, verify=True):
from r2.lib import cssfilter
if g.css_killswitch or (verify and not self.can_change_stylesheet(c.user)):
return (None, None)
parsed, report = cssfilter.validate_css(content)
parsed = parsed.cssText if parsed else ''
return (report, parsed)
def change_css(self, content, parsed, prev=None, reason=None, author=None, force=False):
from r2.models import ModAction
author = author if author else c.user.name
if content is None:
content = ''
try:
wiki = WikiPage.get(self, 'config/stylesheet')
except tdb_cassandra.NotFound:
wiki = WikiPage.create(self, 'config/stylesheet')
wiki.revise(content, previous=prev, author=author, reason=reason, force=force)
self.stylesheet_contents = parsed
self.stylesheet_hash = md5(parsed).hexdigest()
set_last_modified(self, 'stylesheet_contents')
c.site._commit()
ModAction.create(self, c.user, action='wikirevise', details='Updated subreddit stylesheet')
def is_special(self, user):
return (user
@@ -717,6 +799,10 @@ class FakeSubreddit(Subreddit):
self.title = ''
self.link_flair_position = 'right'
@property
def _should_wiki(self):
return False
def is_moderator(self, user):
return c.user_is_loggedin and c.user_is_admin
@@ -906,10 +992,32 @@ class DefaultSR(_DefaultSR):
self._base = Subreddit._by_name(g.default_sr, stale=True)
except NotFound:
self._base = None
@property
def _should_wiki(self):
return True
@property
def wikimode(self):
return self._base.wikimode
@property
def wiki_edit_karma(self):
return self._base.wiki_edit_karma
def is_wikibanned(self, user):
return self._base.is_banned(user)
def is_wikicreate(self, user):
return self._base.is_wikicreate(user)
@property
def _fullname(self):
return "t5_6"
@property
def _id36(self):
return self._base._id36
@property
def type(self):
@@ -1087,7 +1195,9 @@ class SRMember(Relation(Subreddit, Account)): pass
Subreddit.__bases__ += (UserRel('moderator', SRMember),
UserRel('contributor', SRMember),
UserRel('subscriber', SRMember, disable_ids_fn = True),
UserRel('banned', SRMember))
UserRel('banned', SRMember),
UserRel('wikibanned', SRMember),
UserRel('wikicontributor', SRMember))
class SubredditPopularityByLanguage(tdb_cassandra.View):
_use_db = True

356
r2/r2/models/wiki.py Normal file
View File

@@ -0,0 +1,356 @@
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of
# the Original Code is reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2012 reddit
# Inc. All Rights Reserved.
###############################################################################
from datetime import datetime
from r2.lib.db import tdb_cassandra
from r2.lib.db.thing import NotFound
from r2.lib.merge import *
from pycassa.system_manager import TIME_UUID_TYPE
from pylons import c, g
from pylons.controllers.util import abort
from r2.models.printable import Printable
from r2.models.account import Account
from collections import OrderedDict
# Used for the key/id for pages,
PAGE_ID_SEP = '\t'
# Number of days to keep recent revisions for
WIKI_RECENT_DAYS = g.wiki_keep_recent_days
# Max length of a single page in bytes
MAX_PAGE_LENGTH_BYTES = g.wiki_max_page_length_bytes
# Namespaces in which access is denied to do anything but view
restricted_namespaces = ('reddit/', 'config/', 'special/')
# Pages which may only be edited by mods, must be within restricted namespaces
special_pages = ('config/stylesheet', 'config/sidebar', 'config/description')
# Pages which have a special length restrictions (In bytes)
special_length_restrictions_bytes = {'config/stylesheet': 128*1024, 'config/sidebar': 5120, 'config/description': 500}
modactions = {'config/sidebar': "Updated subreddit sidebar"}
# Page "index" in the subreddit "reddit.com" and a seperator of "\t" becomes:
# "reddit.com\tindex"
def wiki_id(sr, page):
return ('%s%s%s' % (sr, PAGE_ID_SEP, page)).lower()
class ContentLengthError(Exception):
def __init__(self, max_length):
Exception.__init__(self)
self.max_length = max_length
class WikiPageExists(Exception):
pass
class WikiPageEditors(tdb_cassandra.View):
_use_db = True
_value_type = 'str'
_connection_pool = 'main'
def get_author_name(author_name):
if not author_name:
return "[unknown]"
try:
return Account._by_name(author_name).name
except NotFound:
return '[deleted]'
class WikiRevision(tdb_cassandra.UuidThing, Printable):
""" Contains content (markdown), author of the edit, page the edit belongs to, and datetime of the edit """
_use_db = True
_connection_pool = 'main'
_str_props = ('pageid', 'content', 'author', 'reason')
_bool_props = ('hidden')
cache_ignore = set(['subreddit'] + list(_str_props)).union(Printable.cache_ignore)
def author_name(self):
return get_author_name(getattr(self, 'author', None))
@classmethod
def add_props(cls, user, wrapped):
for item in wrapped:
item._hidden = item.is_hidden
item._spam = False
item.reported = False
@classmethod
def get(cls, revid, pageid):
wr = cls._byID(revid)
if wr.pageid != pageid:
raise ValueError('Revision is not for the expected page')
return wr
def toggle_hide(self):
self.hidden = not self.is_hidden
self._commit()
return self.hidden
@classmethod
def create(cls, pageid, content, author=None, reason=None):
kw = dict(pageid=pageid, content=content)
if author:
kw['author'] = author
if reason:
kw['reason'] = reason
wr = cls(**kw)
wr._commit()
WikiRevisionsByPage.add_object(wr)
WikiRevisionsRecentBySR.add_object(wr)
return wr
def _on_commit(self):
WikiRevisionsByPage.add_object(self)
WikiRevisionsRecentBySR.add_object(self)
@classmethod
def get_recent(cls, sr, count=100):
return WikiRevisionsRecentBySR.query([sr], count=count)
@property
def is_hidden(self):
return bool(getattr(self, 'hidden', False))
@property
def info(self, sep=PAGE_ID_SEP):
info = self.pageid.split(sep, 1)
try:
return {'sr': info[0], 'page': info[1]}
except IndexError:
g.log.error('Broken wiki page ID "%s" did PAGE_ID_SEP change?', self.pageid)
return {'sr': 'broken', 'page': 'broken'}
@property
def page(self):
return self.info['page']
@property
def sr(self):
return self.info['sr']
class WikiPage(tdb_cassandra.Thing):
""" Contains permissions, current content (markdown), subreddit, and current revision (ID)
Key is subreddit-pagename """
_use_db = True
_connection_pool = 'main'
_read_consistency_level = tdb_cassandra.CL.QUORUM
_write_consistency_level = tdb_cassandra.CL.QUORUM
_date_props = ('last_edit_date')
_str_props = ('revision', 'name', 'last_edit_by', 'content', 'sr')
_int_props = ('permlevel')
_bool_props = ('listed_')
def author_name(self):
return get_author_name(getattr(self, 'last_edit_by', None))
@classmethod
def get(cls, sr, name):
id = getattr(sr, '_id36', None)
if not id:
raise tdb_cassandra.NotFound
return cls._byID(wiki_id(sr._id36, name))
@classmethod
def create(cls, sr, name):
name = name.lower()
kw = dict(sr=sr._id36, name=name, permlevel=0, content='', listed_=False)
page = cls(**kw)
page._commit()
return page
@property
def restricted(self):
return WikiPage.is_restricted(self.name)
@classmethod
def is_restricted(cls, page):
return ("%s/" % page) in restricted_namespaces or page.startswith(restricted_namespaces)
@classmethod
def is_special(cls, page):
return page in special_pages
@property
def special(self):
return WikiPage.is_special(self.name)
def add_to_listing(self):
WikiPagesBySR.add_object(self)
def _on_create(self):
self.add_to_listing()
def _on_commit(self):
self.add_to_listing()
def remove_editor(self, user):
WikiPageEditors._remove(self._id, [user])
def add_editor(self, user):
WikiPageEditors._set_values(self._id, {user: ''})
@classmethod
def get_pages(cls, sr, after=None):
NUM_AT_A_TIME = 1000
pages = WikiPagesBySR.query([sr._id36], after=after, count=NUM_AT_A_TIME)
pages = list(pages)
if len(pages) >= NUM_AT_A_TIME:
return pages + cls.get_pages(sr, after=pages[-1])
return pages
@classmethod
def get_listing(cls, sr, filter_check=None):
"""
Create a tree of pages from their path.
"""
page_tree = OrderedDict()
pages = cls.get_pages(sr)
pages = filter(filter_check, pages)
pages = sorted(pages, key=lambda page: page.name)
for page in pages:
p = page.name.split('/')
cur_node = page_tree
# Loop through all elements of the path except the page name portion
for name in p[:-1]:
next_node = cur_node.get(name)
# If the element did not already exist in the tree, create it
if not next_node:
new_node = OrderedDict()
cur_node[name] = [None, new_node]
else:
# Otherwise, continue through
new_node = next_node[1]
cur_node = new_node
# Get the actual page name portion of the path
pagename = p[-1]
node = cur_node.get(pagename)
# The node may already exist as a path name in the tree
if node:
node[0] = page
else:
cur_node[pagename] = [page, OrderedDict()]
return page_tree
def get_editors(self, properties=None):
try:
return WikiPageEditors._byID(self._id, properties=properties)._values() or []
except tdb_cassandra.NotFoundException:
return []
def has_editor(self, editor):
return bool(self.get_editors(properties=[editor]))
def revise(self, content, previous = None, author=None, force=False, reason=None):
if self.content == content:
return
max_length = special_length_restrictions_bytes.get(self.name, MAX_PAGE_LENGTH_BYTES)
if len(content) > max_length:
raise ContentLengthError(max_length)
revision = getattr(self, 'revision', None)
if not force and (revision and previous != revision):
if previous:
origcontent = WikiRevision.get(previous, pageid=self._id).content
else:
origcontent = ''
try:
content = threewaymerge(origcontent, content, self.content)
except ConflictException as e:
e.new_id = revision
raise e
wr = WikiRevision.create(self._id, content, author, reason)
self.content = content
self.last_edit_by = author
self.last_edit_date = wr.date
self.revision = wr._id
self._commit()
return wr
def change_permlevel(self, permlevel, force=False):
NUM_PERMLEVELS = 3
if permlevel == self.permlevel:
return
if not force and int(permlevel) not in range(NUM_PERMLEVELS):
raise ValueError('Permlevel not valid')
self.permlevel = permlevel
self._commit()
def get_revisions(self, after=None, count=100):
return WikiRevisionsByPage.query([self._id], after=after, count=count)
def _commit(self, *a, **kw):
if not self._id: # Creating a new page
pageid = wiki_id(self.sr, self.name)
try:
WikiPage._byID(pageid)
raise WikiPageExists()
except tdb_cassandra.NotFound:
self._id = pageid
return tdb_cassandra.Thing._commit(self, *a, **kw)
class WikiRevisionsByPage(tdb_cassandra.DenormalizedView):
""" Associate revisions with pages """
_use_db = True
_connection_pool = 'main'
_view_of = WikiRevision
_compare_with = TIME_UUID_TYPE
@classmethod
def _rowkey(cls, wr):
return wr.pageid
class WikiPagesBySR(tdb_cassandra.DenormalizedView):
""" Associate revisions with subreddits, store only recent """
_use_db = True
_connection_pool = 'main'
_view_of = WikiPage
@classmethod
def _rowkey(cls, wp):
return wp.sr
class WikiRevisionsRecentBySR(tdb_cassandra.DenormalizedView):
""" Associate revisions with subreddits, store only recent """
_use_db = True
_connection_pool = 'main'
_view_of = WikiRevision
_compare_with = TIME_UUID_TYPE
_ttl = 60*60*24*WIKI_RECENT_DAYS
@classmethod
def _rowkey(cls, wr):
return wr.sr

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

View File

@@ -15,4 +15,5 @@ $(function() {
r.ui.HelpBubble.init()
r.interestbar.init()
r.apps.init()
r.wiki.init()
})

View File

@@ -105,8 +105,8 @@ function form_error(form) {
}
}
function simple_post_form(form, where, fields, block) {
$.request(where, get_form_fields(form, fields), null, block,
function simple_post_form(form, where, fields, block, callback) {
$.request(where, get_form_fields(form, fields), callback, block,
"json", false, form_error(form));
return false;
};
@@ -157,7 +157,7 @@ function deleteRow(elem) {
/* general things */
function change_state(elem, op, callback, keep) {
function change_state(elem, op, callback, keep, post_callback) {
var form = $(elem).parents("form");
/* look to see if the form has an id specified */
var id = form.find('input[name="id"]');
@@ -166,7 +166,7 @@ function change_state(elem, op, callback, keep) {
else /* fallback on the parent thing */
id = $(elem).thing_id();
simple_post_form(form, op, {id: id});
simple_post_form(form, op, {id: id}, undefined, post_callback);
/* call the callback first before we mangle anything */
if (callback) {
callback(form.length ? form : elem, op);

View File

@@ -0,0 +1,110 @@
r.wiki = {
baseUrl: function() {
base_url = '/wiki'
if (!r.config.is_fake) {
base_url = '/r/' + r.config.post_site + base_url
}
return base_url
},
init: function() {
$('body').delegate('.wiki-page .revision_hide', 'click', this.toggleHide)
},
toggleHide: function(event) {
event.preventDefault()
var $this = $(this),
url = r.wiki.baseUrl() + '/api/hide/' + $this.data('revision') + '/' + $this.data('page'),
$this_parent = $this.parents('.revision')
$this_parent.toggleClass('hidden')
$.ajax({
url: url,
type: 'POST',
dataType: 'json',
error: function() {
$this_parent.toggleClass('hidden')
},
success: function(data) {
if (!data.status) {
$this_parent.removeClass('hidden')
} else {
$this_parent.addClass('hidden')
}
}
})
},
addUser: function(event) {
event.preventDefault()
$('#usereditallowerror').hide()
var $this = $(event.target),
url = r.wiki.baseUrl() + '/api/alloweditor/add/' + $this.find('[name="username"]').val() + '/' + $this.data('page')
$.ajax({
url: url,
type: 'POST',
dataType: 'json',
error: function() {
$('#usereditallowerror').show()
},
success: function(data) {
location.reload()
}
})
},
submitEdit: function(event) {
event.preventDefault()
var $this = $(event.target),
url = r.wiki.baseUrl() + '/api/edit/' + $this.data('page'),
conflict = $('#wiki_edit_conflict'),
special = $('#wiki_special_error')
conflict.hide()
special.hide()
$.ajax({
url: url,
type: 'POST',
dataType: 'json',
data: $this.serialize(),
success: function() {
window.location = r.wiki.baseUrl() + '/' + $this.data('page')
},
statusCode: {
409: function(xhr) {
var info = JSON.parse(xhr.responseText)
,content = $this.children('#content')
conflict.children('#youredit').val(content.val())
conflict.children('#yourdiff').html(info.diffcontent)
$this.children('#previous').val(info.newrevision)
content.val(info.newcontent)
conflict.fadeIn('slow')
},
415: function(xhr) {
var errors = JSON.parse(xhr.responseText).special_errors
,specials = special.children('#specials')
specials.empty()
for(i in errors) {
specials.append(errors[i]+'<br/>')
}
special.fadeIn('slow')
},
429: function(xhr) {
var message = JSON.parse(xhr.responseText).message
,specials = special.children('#specials')
specials.empty()
specials.text(message)
special.fadeIn('slow')
}
}
})
},
goCompare: function(page, base) {
v1 = $('input:radio[name=v1]:checked').val()
v2 = $('input:radio[name=v2]:checked').val()
url = base + '/' + page + '?v=' + v1
if (v2 != v1) {
url += '&v2=' + v2
}
window.location = url
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

View File

@@ -72,6 +72,11 @@
%else:
${UserText(None, text="", creating=True, name="public_description", have_form=False)}
%endif
<div id="public_description_conflict_box" style="display: none">
<div id="public_description_conflict_diff"></div>
${UserText(None, text ="", editable = True, creating = True, name="public_description_conflict_old", have_form = False)}
</div>
${error_field("CONFLICT", "public_description")}
</%utils:line_field>
<%utils:line_field title="${_('sidebar')}" css_class="usertext"
@@ -81,7 +86,18 @@
%else:
${UserText(None, text = "", creating = True, name="description", have_form = False)}
%endif
<div id="description_conflict_box" style="display: none">
<div id="description_conflict_diff"></div>
${UserText(None, text ="", editable = True, creating = True, name="description_conflict_old", have_form = False)}
</div>
${error_field("CONFLICT", "description")}
</%utils:line_field>
%if thing.site:
<input type="hidden" name="prev_public_description_id" value="${thing.site.prev_public_description_id}"/>
<input type="hidden" name="prev_description_id" value="${thing.site.prev_description_id}"/>
%endif
<%utils:line_field title="${_('language')}">
<div class="delete-field">
@@ -132,7 +148,41 @@
</table>
</div>
</%utils:line_field>
<%utils:line_field title="${_('wiki')}">
<div class="delete-field">
<table>
${utils.radio_type('wikimode', "disabled", _("disabled"),
_("Wiki is disabled for all users execept mods"),
(not thing.site or thing.site.wikimode == 'disabled'))}
${utils.radio_type('wikimode', "modonly", _("mod editing"),
_("Only mods or those on a pages edit list may edit"),
(thing.site and thing.site.wikimode == 'modonly'))}
${utils.radio_type('wikimode', "anyone", _("anyone"),
_("Anyone who can submit to the subreddit may edit"),
(thing.site and thing.site.wikimode == 'anyone'))}
</table>
</div>
<div class="usertext-edit">
<div class="delete-field">
<label for="wiki_edit_karma">${_('Subreddit karma required to edit and create wiki pages:')}</label>
%if thing.site:
<input id="wiki_edit_karma" type="text" name="wiki_edit_karma"
value = "${getattr(thing.site, 'wiki_edit_karma', 100)}"/>
%else:
<input id="wiki_edit_karma" type="text" name="wiki_edit_karma" value="100" />
%endif
</div>
<div class="delete-field">
<label for="wiki_edit_age">${_('Account age (days) required to edit and create wiki pages:')}</label>
%if thing.site:
<input id="wiki_edit_age" type="text" name="wiki_edit_age"
value = "${getattr(thing.site, 'wiki_edit_age', 0)}"/>
%else:
<input id="wiki_edit_age" type="text" name="wiki_edit_age" value="0" />
%endif
</div>
</div>
</%utils:line_field>
<%utils:line_field title="${_('other options')}">
<div class="delete-field">
<ul>

View File

@@ -411,6 +411,7 @@
<%def name="ynbutton(title, executed, op, callback = 'null',
question = None,
post_callback = 'null',
format = '%(link)s',
format_arg = 'link',
hidden_data = {},
@@ -434,7 +435,7 @@
<span class="option error">
${question}
&#32;<a href="javascript:void(0)" class="yes"
onclick='change_state(this, "${op}", ${callback})'>
onclick='change_state(this, "${op}", ${callback}, undefined, ${post_callback})'>
${_("yes")}
</a>&#32;/&#32;
<a href="javascript:void(0)" class="no"

View File

@@ -41,6 +41,7 @@
<input type="hidden" name="op" value="" />
<h2>${_("stylesheet")}</h2>
<input type="hidden" name="prevstyle" value="${thing.site.prev_stylesheet}"/>
<div class="sheets">
<div style="width: 100%" class="col">
<div>
@@ -52,6 +53,11 @@
>
${keep_space(thing.stylesheet_contents) or ''}
</textarea>
<div id="conflict_box" style="display: none">
<div id="conflict_diff"></div>
<h2 class="error">${_('Your edit will not be saved!')}</h2>
<textarea rows="20" cols="20" name="conflict_old"></textarea>
</div>
</div>
</div>
<div style="display:none" class="col">

View File

@@ -0,0 +1,67 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%def name="actionsbar(actions)">
%for action in actions:
<a class="wikiaction wikiaction-${action[0]}
%if action[0] == thing.action[0]:
wikiaction-current
%endif
"
%if action[2]:
href="${thing.base_url}/${action[0]}/${c.page}"
%else:
href="${thing.base_url}/${action[0]}"
%endif
>${action[1]}</a>
%endfor
</%def>
<span>
<h1 class="wikititle">
%if thing.title:
${thing.title}
%endif
%if c.page:
<strong>${c.page}</strong>
%endif
</h1>
%if thing.pageactions:
<span class="pageactions">
${actionsbar(thing.pageactions)}
</span>
%endif
</span>
<br/>
%if thing.description:
<br/>
<span><h2>${unsafe(thing.description)}</h2></span>
%endif
<div class="wiki-page-content">
${thing.content}
</div>
<!--Reddit wikis are powered by Cray-1™ supercomputers-->

View File

@@ -0,0 +1,23 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
${thing.content}

View File

@@ -0,0 +1,49 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%!
from r2.lib.filters import keep_space
%>
<div style="display: none;" id="wiki_edit_conflict">
<h2 class="error">there was a conflict editing</h2>
<h1>${_("your edit")}</h1>
<em>${_("this edit is for you to resolve the conflict, any text in the box below will not save.")}</em><br/>
<textarea id="youredit"></textarea>
<span id="yourdiff"></span>
<h1>${_("current edit")}</h1>
</div>
<div style="display: none;" id="wiki_special_error">
<h1>Errors: </h1>
<span id="specials" class="error"></span>
</div>
<form method="post" id="editform" data-page="${c.page}" onsubmit="r.wiki.submitEdit(event)">
<textarea name="content" rows="20" cols="20" style="width: 100%" id="content">${keep_space(thing.page_content)}</textarea>
<br/><br/>
<label for="reason">${_("reason for revision")}</label><br/>
<input type="text" name="reason" style="width: 100%" />
<input type="hidden" id="previous" name="previous" value="${thing.previous}" /><br/><br/>
<input type="submit" value="${_('save page')}" />
<input type="button" value="${_('cancel edit')}" onclick="location.href='${thing.base_url}/${c.page}'" />
</form>

View File

@@ -0,0 +1,34 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%!
from r2.lib.template_helpers import get_domain
%>
${thing.listing}
<div class="morelink discussionlink">
<a href="/submit?url=http://${get_domain()}/wiki/${c.page}&resubmit=true&no_self=true&title=Check+out+this+wiki+page">${_("submit a discussion")}
<div class="nub"></div>
</a>
</div>

View File

@@ -0,0 +1,42 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<% from r2.models.wiki import WikiPage %>
<%def name="listing(pages)">
%for name, page_info in pages.iteritems():
<ul>
<li>
%if page_info[0]:
<a href="${c.wiki_base_url}/${page_info[0].name}" target="_blank">${name}</a></li>
%else:
${name}
</li>
%endif
${listing(page_info[1])}
</ul>
%endfor
</%def>
<div class="pagelisting">
${listing(thing.pages)}
</div>

View File

@@ -0,0 +1,27 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
${thing.revisions}
%if c.page and thing.revisions.things:
<button onclick="r.wiki.goCompare('${c.page}', '${c.wiki_base_url}')">${_("compare selected")}</button>
%endif
<br/>

View File

@@ -0,0 +1,23 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
${thing.revisions}

View File

@@ -0,0 +1,70 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%namespace file="printablebuttons.html" import="ynbutton" />
<%namespace name="utils" file="utils.html"/>
<div class="fancy-settings">
%if thing.show_settings:
<form id="pagesettings" method="post">
<%utils:line_field title=" ${_('who can edit this page?')}">
<input type="radio" name="permlevel" id="permlevel0" value="0"
%if thing.permlevel == 0:
checked
%endif
/><label for="permlevel0">${_('anyone may edit')}</label><br/>
<input type="radio" name="permlevel" id="permlevel1" value="1"
%if thing.permlevel == 1:
checked
%endif
/><label for="permlevel1">${_('only approved wiki contributors may edit')}</label><br/>
<input type="radio" name="permlevel" id="permlevel2" value="2"
%if thing.permlevel == 2:
checked
%endif
/><label for="permlevel2">${_('only mods may edit and view')}</label><br/>
</%utils:line_field>
</form>
%endif
%if thing.permlevel != 2:
<br/>
<%utils:line_field title="${_('allow users to edit page')}">
<form id="WikiAllowEditor" data-page="${c.page}" onsubmit="r.wiki.addUser(event)">
<input name="username" maxlength="32" type="text" style="width: 430px;" />
<button type="submit" style="font-size: 100%;">${_('add')}</button>
<h3 class="error" style="display:none" id="usereditallowerror">${_('username does not exist')}</h2>
</form>
<br/>
<ul>
%for user in thing.mayedit:
<li>
${user}
&mdash;&nbsp;
${ynbutton(_("(remove)"), _("done"), "../r/%s/wiki/api/alloweditor/del/%s/%s" % (c.site.name, user, c.page), post_callback="$.refresh")}
</li>
%endfor
</ul>
</%utils:line_field>
%endif
<input type="submit" onclick="$('#pagesettings').submit()" value="${_('save settings')}" />
</div>

View File

@@ -0,0 +1,70 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%namespace file="utils.html" import="timestamp"/>
<%namespace file="printablebuttons.html" import="ynbutton" />
<tr class="revision
%if thing._hidden:
hidden
%endif
">
%if c.page:
<td style="white-space: nowrap;">
<input type="radio" name="v1" value="${thing._id}" checked="yes">
<input type="radio" name="v2" value="${thing._id}" checked="yes">
</td>
%endif
<td style="white-space: nowrap;">
${timestamp(thing.date)}&nbsp;ago
</td>
%if not c.page:
<td>
<a href="/r/${thing.sr}/wiki/${thing.page}">${thing.page}</a>
</td>
%endif
<td>
<a href="/r/${thing.sr}/wiki/${thing.page}?v=${thing._id}">view</a>
</td>
<td>
${thing.author_name()}
</td>
<td style="font-style: italic;">
${thing._get('reason')}
</td>
%if c.page and c.is_wiki_mod:
<td>
<a href="#" class="revision_hide" data-revision="${thing._id}" data-page="${thing.page}">hide</a>
</td>
<td class="wiki_revert" style="white-space: nowrap;">
${ynbutton(_("revert here"), _("done"), "../r/%s/wiki/api/revert/%s/%s" % (thing.sr, thing._id, thing.page), post_callback="$.refresh")}
</td>
%endif
</tr>

View File

@@ -0,0 +1,30 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<item>
<title>${thing._id}</title>
<author>${thing._get('author', 'Unknown')}</author>
<category>${thing.sr}/${thing.page}</category>
<description>${thing._get('reason')}</description>
<pubdate>${thing.date}</pubdate>
<guid>${thing._id}</guid>
</item>

View File

@@ -0,0 +1,44 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%namespace file="utils.html" import="timestamp"/>
%if thing.diff:
<p>
${unsafe(thing.diff)}
</p>
%endif
<p>
%if not thing.page_content:
<em>${_("this page is empty")}</em>
%else:
${unsafe(thing.page_content)}
%endif
</p>
%if thing.edit_date:
<hr/>
<em>
${_("revision by %s") % thing.edit_by}
&mdash;&nbsp;${timestamp(thing.edit_date)}&nbsp;ago
</em>
%endif

View File

@@ -93,14 +93,14 @@ setup(
"pylibmc==1.2.1-dev",
"py-bcrypt",
"python-statsd",
"snudown",
"snudown>=1.1.0",
"l2cs",
"lxml",
"kazoo",
],
dependency_links=[
"https://github.com/downloads/reddit/pylibmc/pylibmc-1.2.1-dev.tar.gz#egg=pylibmc-1.2.1-dev",
"https://nodeload.github.com/reddit/snudown/tarball/v1.0.4#egg=snudown-1.0.4",
"https://nodeload.github.com/reddit/snudown/tarball/v1.1.0#egg=snudown-1.1.0",
"https://nodeload.github.com/reddit/pycassa/zipball/master#egg=pycassa-1.7.0",
],
packages=find_packages(exclude=["ez_setup"]),