mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-04-27 03:00:12 -04:00
Wiki: Base wiki code
- Updates snudown dep
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
274
r2/r2/controllers/validator/wiki.py
Normal file
274
r2/r2/controllers/validator/wiki.py
Normal 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
299
r2/r2/controllers/wiki.py
Normal 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')
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('&', "&").replace("<", "<").replace(">", ">").replace('"', """)
|
||||
@@ -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)
|
||||
|
||||
@@ -282,6 +282,7 @@ module["reddit"] = LocalizedModule("reddit.js",
|
||||
"analytics.js",
|
||||
"flair.js",
|
||||
"interestbar.js",
|
||||
"wiki.js",
|
||||
"reddit.js",
|
||||
"apps.js",
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
142
r2/r2/lib/pages/wiki.py
Normal 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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')}
|
||||
|
||||
|
||||
@@ -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
356
r2/r2/models/wiki.py
Normal 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
|
||||
|
||||
|
||||
BIN
r2/r2/public/static/house.png
Normal file
BIN
r2/r2/public/static/house.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 806 B |
BIN
r2/r2/public/static/icons/report.png
Normal file
BIN
r2/r2/public/static/icons/report.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 649 B |
@@ -15,4 +15,5 @@ $(function() {
|
||||
r.ui.HelpBubble.init()
|
||||
r.interestbar.init()
|
||||
r.apps.init()
|
||||
r.wiki.init()
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
110
r2/r2/public/static/js/wiki.js
Normal file
110
r2/r2/public/static/js/wiki.js
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
r2/r2/public/static/page_white_copy.png
Normal file
BIN
r2/r2/public/static/page_white_copy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 309 B |
BIN
r2/r2/public/static/report.png
Normal file
BIN
r2/r2/public/static/report.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 649 B |
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
 <a href="javascript:void(0)" class="yes"
|
||||
onclick='change_state(this, "${op}", ${callback})'>
|
||||
onclick='change_state(this, "${op}", ${callback}, undefined, ${post_callback})'>
|
||||
${_("yes")}
|
||||
</a> / 
|
||||
<a href="javascript:void(0)" class="no"
|
||||
|
||||
@@ -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">
|
||||
|
||||
67
r2/r2/templates/wikibasepage.html
Normal file
67
r2/r2/templates/wikibasepage.html
Normal 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-->
|
||||
23
r2/r2/templates/wikibasepage.xml
Normal file
23
r2/r2/templates/wikibasepage.xml
Normal 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}
|
||||
49
r2/r2/templates/wikieditpage.html
Normal file
49
r2/r2/templates/wikieditpage.html
Normal 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>
|
||||
34
r2/r2/templates/wikipagediscussions.html
Normal file
34
r2/r2/templates/wikipagediscussions.html
Normal 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>
|
||||
|
||||
42
r2/r2/templates/wikipagelisting.html
Normal file
42
r2/r2/templates/wikipagelisting.html
Normal 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>
|
||||
27
r2/r2/templates/wikipagerevisions.html
Normal file
27
r2/r2/templates/wikipagerevisions.html
Normal 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/>
|
||||
23
r2/r2/templates/wikipagerevisions.xml
Normal file
23
r2/r2/templates/wikipagerevisions.xml
Normal 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}
|
||||
70
r2/r2/templates/wikipagesettings.html
Normal file
70
r2/r2/templates/wikipagesettings.html
Normal 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}
|
||||
—
|
||||
${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>
|
||||
70
r2/r2/templates/wikirevision.html
Normal file
70
r2/r2/templates/wikirevision.html
Normal 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)} 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>
|
||||
30
r2/r2/templates/wikirevision.xml
Normal file
30
r2/r2/templates/wikirevision.xml
Normal 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>
|
||||
44
r2/r2/templates/wikiview.html
Normal file
44
r2/r2/templates/wikiview.html
Normal 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}
|
||||
— ${timestamp(thing.edit_date)} ago
|
||||
</em>
|
||||
%endif
|
||||
@@ -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"]),
|
||||
|
||||
Reference in New Issue
Block a user