mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-01-27 15:58:06 -05:00
added the ability for subreddit moderators to upload custom images for use in their custom stylesheets
This commit is contained in:
@@ -66,6 +66,7 @@ stylesheet_rtl = reddit_rtl.css
|
||||
|
||||
allowed_css_linked_domains = my.domain.com, my.otherdomain.com
|
||||
css_killswitch = False
|
||||
max_sr_images = 20
|
||||
|
||||
login_cookie = reddit_session
|
||||
domain = localhost
|
||||
|
||||
@@ -333,7 +333,7 @@ class LimitUploadSize(object):
|
||||
or int(environ[cl_key]) > self.max_size):
|
||||
r = Response()
|
||||
r.status_code = 500
|
||||
r.content = 'request too big'
|
||||
r.content = '<html><head></head><body><script type="text/javascript">parent.too_big();</script>request too big</body></html>'
|
||||
return r(environ, start_response)
|
||||
|
||||
return self.app(environ, start_response)
|
||||
|
||||
@@ -774,9 +774,13 @@ class ApiController(RedditController):
|
||||
res._update('status', innerHTML = '')
|
||||
res._update('validation-errors', innerHTML = '')
|
||||
|
||||
stylesheet_contents_parsed = parsed.cssText if parsed else ''
|
||||
# if the css parsed, we're going to apply it (both preview & save)
|
||||
if not report.errors:
|
||||
res._call('applyStylesheet("%s"); ' %
|
||||
stylesheet_contents_parsed.replace('"', r"\"").replace("\n", r"\n").replace("\r", r"\r"))
|
||||
if not report.errors and op == 'save':
|
||||
stylesheet_contents_user = stylesheet_contents
|
||||
stylesheet_contents_parsed = parsed.cssText if parsed else ''
|
||||
|
||||
c.site.stylesheet_contents = stylesheet_contents_parsed
|
||||
c.site.stylesheet_contents_user = stylesheet_contents_user
|
||||
@@ -788,7 +792,6 @@ class ApiController(RedditController):
|
||||
c.site._commit()
|
||||
|
||||
res._update('status', innerHTML = 'saved')
|
||||
res._call('applyStylesheetFromTextbox("stylesheet_contents");')
|
||||
res._update('validation-errors', innerHTML = '')
|
||||
|
||||
elif op == 'preview':
|
||||
@@ -820,36 +823,121 @@ class ApiController(RedditController):
|
||||
return
|
||||
cssfilter.rendered_comment('preview_comment',res,comments)
|
||||
|
||||
@validate(VUser(),
|
||||
@Json
|
||||
@validate(VSrModerator(),
|
||||
VModhash(),
|
||||
VRatelimit(rate_user = True,
|
||||
rate_ip = True,
|
||||
prefix = 'upload_reddit_img_'),
|
||||
file = VLength('file',length=1024*500),
|
||||
op = VOneOf('op',['upload','delete']))
|
||||
def POST_upload_header_img(self, file, op):
|
||||
if not c.site.can_change_stylesheet(c.user):
|
||||
return self.abort403()
|
||||
|
||||
name = VCssName('img_name'))
|
||||
def POST_delete_sr_img(self, res, name):
|
||||
"""
|
||||
Called called upon requested delete on /about/stylesheet.
|
||||
Updates the site's image list, and causes the <li> which wraps
|
||||
the image to be hidden.
|
||||
"""
|
||||
# just in case we need to kill this feature from XSS
|
||||
if g.css_killswitch:
|
||||
return self.abort(403,'forbidden')
|
||||
c.site.del_image(name)
|
||||
c.site._commit()
|
||||
# hide the image and it's container
|
||||
res._hide("img-li_%s" % name)
|
||||
# reset the status
|
||||
res._update('img-status', innerHTML = _("deleted"))
|
||||
|
||||
|
||||
if op == 'upload':
|
||||
try:
|
||||
cleaned = cssfilter.clean_image(file,'PNG')
|
||||
new_url = cssfilter.save_header_image(c.site, cleaned)
|
||||
except cssfilter.BadImage:
|
||||
return UploadedImage(_('bad image'),c.site.header,'upload').render()
|
||||
|
||||
c.site.header = new_url
|
||||
c.site._commit()
|
||||
|
||||
return UploadedImage(_('saved'),new_url,'upload').render()
|
||||
elif op == 'delete':
|
||||
@Json
|
||||
@validate(VSrModerator(),
|
||||
VModhash())
|
||||
def POST_delete_sr_header(self, res):
|
||||
"""
|
||||
Called when the user request that the header on a sr be reset.
|
||||
"""
|
||||
# just in case we need to kill this feature from XSS
|
||||
if g.css_killswitch:
|
||||
return self.abort(403,'forbidden')
|
||||
if c.site.header:
|
||||
c.site.header = None
|
||||
c.site._commit()
|
||||
# reset the header image on the page
|
||||
res._update('header-img', src = DefaultSR.header)
|
||||
# hide the button which started this
|
||||
res._hide ('delete-img')
|
||||
# hide the preview box
|
||||
res._hide ('img-preview-container')
|
||||
# reset the status boxes
|
||||
res._update('img-status', innerHTML = _("deleted"))
|
||||
res._update('status', innerHTML = "")
|
||||
|
||||
|
||||
return UploadedImage(_('deleted'),DefaultSR.header,'delete').render()
|
||||
def GET_upload_sr_img(self, *a, **kw):
|
||||
"""
|
||||
Completely unnecessary method which exists because safari can
|
||||
be dumb too. On page reload after an image has been posted in
|
||||
safari, the iframe to which the request posted preserves the
|
||||
URL of the POST, and safari attempts to execute a GET against
|
||||
it. The iframe is hidden, so what it returns is completely
|
||||
irrelevant.
|
||||
"""
|
||||
return "nothing to see here."
|
||||
|
||||
@validate(VSrModerator(),
|
||||
VModhash(),
|
||||
file = VLength('file', length=1024*500),
|
||||
name = VCssName("name"),
|
||||
header = nop('header'))
|
||||
def POST_upload_sr_img(self, file, header, name):
|
||||
"""
|
||||
Called on /about/stylesheet when an image needs to be replaced
|
||||
or uploaded, as well as on /about/edit for updating the
|
||||
header. Unlike every other POST in this controller, this
|
||||
method does not get called with Ajax but rather is from the
|
||||
original form POSTing to a hidden iFrame. Unfortunately, this
|
||||
means the response needs to generate an page with a script tag
|
||||
to fire the requisite updates to the parent document, and,
|
||||
more importantly, that we can't use our normal toolkit for
|
||||
passing those responses back.
|
||||
|
||||
The result of this function is a rendered UploadedImage()
|
||||
object in charge of firing the completedUploadImage() call in
|
||||
JS.
|
||||
"""
|
||||
|
||||
# default error list (default valuse will reset the errors in
|
||||
# the response if no error is raised)
|
||||
errors = dict(BAD_CSS_NAME = "", IMAGE_ERROR = "")
|
||||
try:
|
||||
cleaned = cssfilter.clean_image(file,'PNG')
|
||||
if header:
|
||||
num = None # there is one and only header, and it is unnumbered
|
||||
elif not name:
|
||||
# error if the name wasn't specified or didn't satisfy
|
||||
# the validator
|
||||
errors['BAD_CSS_NAME'] = _("bad image name")
|
||||
else:
|
||||
num = c.site.add_image(name, max_num = g.max_sr_images)
|
||||
c.site._commit()
|
||||
|
||||
except cssfilter.BadImage:
|
||||
# if the image doesn't clean up nicely, abort
|
||||
errors["IMAGE_ERROR"] = _("bad image")
|
||||
except ValueError:
|
||||
# the add_image method will raise only on too many images
|
||||
errors['IMAGE_ERROR'] = (
|
||||
_("too many imags (you only get %d)") % g.max_sr_images)
|
||||
|
||||
if any(errors.values()):
|
||||
return UploadedImage("", "", "", errors = errors).render()
|
||||
else:
|
||||
# with the image num, save the image an upload to s3. the
|
||||
# header image will be of the form "${c.site._fullname}.png"
|
||||
# while any other image will be ${c.site._fullname}_${num}.png
|
||||
new_url = cssfilter.save_sr_image(c.site, cleaned, num = num)
|
||||
if header:
|
||||
c.site.header = new_url
|
||||
c.site._commit()
|
||||
|
||||
return UploadedImage(_('saved'), new_url, name,
|
||||
errors = errors).render()
|
||||
|
||||
|
||||
@Json
|
||||
@validate(VUser(),
|
||||
|
||||
@@ -511,7 +511,16 @@ class VInt(Validator):
|
||||
except ValueError:
|
||||
c.errors.add(errors.BAD_NUMBER)
|
||||
|
||||
|
||||
class VCssName(Validator):
|
||||
"""
|
||||
returns a name iff it consists of alphanumeric characters and
|
||||
possibly "-", and is below the length limit.
|
||||
"""
|
||||
r_css_name = re.compile(r"^[a-zA-Z0-9\-]{1,100}$")
|
||||
def run(self, name):
|
||||
if name and self.r_css_name.match(name):
|
||||
return name
|
||||
|
||||
class VMenu(Validator):
|
||||
|
||||
def __init__(self, param, menu_cls, remember = True, **kw):
|
||||
|
||||
@@ -42,6 +42,7 @@ class Globals(object):
|
||||
'max_comments',
|
||||
'num_side_reddits',
|
||||
'num_query_queue_workers',
|
||||
'max_sr_images',
|
||||
]
|
||||
|
||||
bool_props = ['debug', 'translator',
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
from r2.models import *
|
||||
from r2.lib.utils import sanitize_url, domain
|
||||
from r2.lib.utils import sanitize_url, domain, randstr
|
||||
from r2.lib.strings import string_dict
|
||||
|
||||
from pylons import g
|
||||
from pylons import g, c
|
||||
from pylons.i18n import _
|
||||
|
||||
import re
|
||||
@@ -152,11 +152,37 @@ class ValidationError(Exception):
|
||||
obj = str(self.obj) if hasattr(self,'obj') else ''
|
||||
return "ValidationError%s: %s (%s)" % (line, self.message, obj)
|
||||
|
||||
# local urls should be in the static directory
|
||||
local_urls = re.compile(r'^/static/[a-z./-]+$')
|
||||
# substitutable urls will be css-valid labels surrounded by "%%"
|
||||
custom_img_urls = re.compile(r'%%([a-zA-Z0-9\-]+)%%')
|
||||
def valid_url(prop,value,report):
|
||||
"""
|
||||
checks url(...) arguments in CSS, ensuring that the contents are
|
||||
officially sanctioned. Sanctioned urls include:
|
||||
* anything in /static/
|
||||
* image labels %%..%% for images uploaded on /about/stylesheet
|
||||
* urls with domains in g.allowed_css_linked_domains
|
||||
"""
|
||||
url = value.getStringValue()
|
||||
# local urls are allowed
|
||||
if local_urls.match(url):
|
||||
pass
|
||||
# custom urls are allowed, but need to be transformed into a real path
|
||||
elif custom_img_urls.match(url):
|
||||
name = custom_img_urls.match(url).group(1)
|
||||
# the label -> image number lookup is stored on the subreddit
|
||||
if c.site.images.has_key(name):
|
||||
num = c.site.images[name]
|
||||
value._setCssText("url(http:/%s%s_%d.png?v=%s)"
|
||||
% (g.s3_thumb_bucket, c.site._fullname, num,
|
||||
randstr(36)))
|
||||
else:
|
||||
# unknown image label -> error
|
||||
report.append(ValidationError(msgs['broken_url']
|
||||
% dict(brokenurl = value.cssText),
|
||||
value))
|
||||
# allowed domains are ok
|
||||
elif domain(url) in g.allowed_css_linked_domains:
|
||||
pass
|
||||
else:
|
||||
@@ -367,7 +393,13 @@ def clean_image(data,format):
|
||||
|
||||
return ret
|
||||
|
||||
def save_header_image(sr, data):
|
||||
def save_sr_image(sr, data, num = None):
|
||||
"""
|
||||
uploades image data to s3 as a PNG and returns its new url. Urls
|
||||
will be of the form:
|
||||
http:/${g.s3_thumb_bucket}/${sr._fullname}[_${num}].png?v=${md5hash}
|
||||
[Note: g.s3_thumb_bucket begins with a "/" so the above url is valid.]
|
||||
"""
|
||||
import tempfile
|
||||
from r2.lib import s3cp
|
||||
from md5 import md5
|
||||
@@ -379,12 +411,18 @@ def save_header_image(sr, data):
|
||||
f.write(data)
|
||||
f.flush()
|
||||
|
||||
resource = g.s3_thumb_bucket + sr._fullname + '.png'
|
||||
s3cp.send_file(f.name, resource, 'image/png', 'public-read', None, False)
|
||||
resource = g.s3_thumb_bucket + sr._fullname
|
||||
if num is not None:
|
||||
resource += '_' + str(num)
|
||||
resource += '.png'
|
||||
|
||||
s3cp.send_file(f.name, resource, 'image/png', 'public-read',
|
||||
None, False)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
return 'http:/%s%s.png?v=%s' % (g.s3_thumb_bucket, sr._fullname, hash)
|
||||
return 'http:/%s%s?v=%s' % (g.s3_thumb_bucket,
|
||||
resource.split('/')[-1], hash)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -684,9 +684,9 @@ class CssError(Wrapped):
|
||||
|
||||
class UploadedImage(Wrapped):
|
||||
"The page rendered in the iframe during an upload of a header image"
|
||||
def __init__(self,status,img_src,op):
|
||||
Wrapped.__init__(self,
|
||||
status=status, img_src=img_src, op=op)
|
||||
def __init__(self,status,img_src, name="", errors = {}):
|
||||
self.errors = list(errors.iteritems())
|
||||
Wrapped.__init__(self, status=status, img_src=img_src, name = name)
|
||||
|
||||
class Password(Wrapped):
|
||||
"""Form encountered when 'recover password' is clicked in the LoginFormWide."""
|
||||
|
||||
@@ -495,7 +495,6 @@ class UrlParser(object):
|
||||
unquote_plus('='.join(p[1:])))
|
||||
self._query_dict = dict(_split(p) for p in self.query.split('&')
|
||||
if p)
|
||||
|
||||
return self._query_dict
|
||||
|
||||
def path_extension(self):
|
||||
|
||||
@@ -66,7 +66,7 @@ class Wrapped(object):
|
||||
for lookup in self.lookups:
|
||||
try:
|
||||
template = tpm.get(lookup, style, cache = not debug)
|
||||
except KeyError:
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
|
||||
@@ -44,6 +44,7 @@ class Subreddit(Thing, Printable):
|
||||
firsttext = strings.firsttext,
|
||||
header = os.path.join(g.static_path,
|
||||
'base.reddit.com.header.png'),
|
||||
images = {},
|
||||
ad_file = os.path.join(g.static_path, 'ad_default.html'),
|
||||
reported = 0,
|
||||
valid_votes = 0,
|
||||
@@ -69,6 +70,7 @@ class Subreddit(Thing, Printable):
|
||||
clear_memo('subreddit.subreddits', Subreddit)
|
||||
return sr
|
||||
|
||||
|
||||
@classmethod
|
||||
@memoize('subreddit._by_name')
|
||||
def _by_name_cache(cls, name):
|
||||
@@ -348,6 +350,69 @@ class Subreddit(Thing, Printable):
|
||||
user = c.user if c.user_is_loggedin else None
|
||||
return self.can_view(user)
|
||||
|
||||
def get_images(self):
|
||||
"""
|
||||
Iterator over list of (name, image_num) pairs which have been
|
||||
uploaded for custom styling of this subreddit.
|
||||
"""
|
||||
for name, img_num in self.images.iteritems():
|
||||
if isinstance(img_num, int):
|
||||
yield (name, img_num)
|
||||
|
||||
def add_image(self, name, max_num = None):
|
||||
"""
|
||||
Adds an image to the subreddit's image list. The resulting
|
||||
number of the image is returned. Note that image numbers are
|
||||
non-sequential insofar as unused numbers in an existing range
|
||||
will be populated before a number outside the range is
|
||||
returned. Imaged deleted with del_image are pushed onto the
|
||||
"/empties/" stack in the images dict, and those values are
|
||||
pop'd until the stack is empty.
|
||||
|
||||
raises ValueError if the resulting number is >= max_num.
|
||||
|
||||
The Subreddit will be _dirty if a new image has been added to
|
||||
its images list, and no _commit is called.
|
||||
"""
|
||||
if not self.images.has_key(name):
|
||||
# copy and blank out the images list to flag as _dirty
|
||||
l = self.images
|
||||
self.images = None
|
||||
# initialize the /empties/ list
|
||||
l.setdefault('/empties/', [])
|
||||
try:
|
||||
num = l['/empties/'].pop() # grab old number if we can
|
||||
except IndexError:
|
||||
num = len(l) - 1 # one less to account for /empties/ key
|
||||
if max_num is not None and num >= max_num:
|
||||
raise ValueError, "too many images"
|
||||
# update the dictionary and rewrite to images attr
|
||||
l[name] = num
|
||||
self.images = l
|
||||
else:
|
||||
# we've seen the image before, so just return the existing num
|
||||
num = self.images[name]
|
||||
return num
|
||||
|
||||
def del_image(self, name):
|
||||
"""
|
||||
Deletes an image from the images dictionary assuming an image
|
||||
of that name is in the current dictionary. The freed up
|
||||
number is pushed onto the /empties/ stack for later recycling
|
||||
by add_image.
|
||||
|
||||
The Subreddit will be _dirty if image has been removed from
|
||||
its images list, and no _commit is called.
|
||||
"""
|
||||
if self.images.has_key(name):
|
||||
l = self.images
|
||||
self.images = None
|
||||
l.setdefault('/empties/', [])
|
||||
# push the number on the empties list
|
||||
l['/empties/'].append(l[name])
|
||||
del l[name]
|
||||
self.images = l
|
||||
|
||||
class FakeSubreddit(Subreddit):
|
||||
over_18 = False
|
||||
title = ''
|
||||
|
||||
@@ -1302,6 +1302,50 @@ a.star { text-decoration: none; color: #ff8b60 }
|
||||
|
||||
.stylesheet-customize-container { }
|
||||
.stylesheet-customize-container textarea { margin: 0; padding: 0px; }
|
||||
.stylesheet-customize-container h2 { margin-top: 15px; margin-bottom: 10px; }
|
||||
|
||||
.image-upload .new-image { margin-left: 20px }
|
||||
.image-upload table { margin-left: 20px; }
|
||||
.image-upload td,
|
||||
.image-upload th { vertical-align: top; }
|
||||
.image-upload span { padding-left: 5px; }
|
||||
|
||||
|
||||
ul#image-preview-list {
|
||||
margin: 20px 320px 20px 20px;
|
||||
font-size:larger;
|
||||
}
|
||||
ul#image-preview-list li {
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
vertical-align: top;
|
||||
width: 45%;
|
||||
height: 100px;
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ul#image-preview-list .preview {
|
||||
width: 100px;
|
||||
float: left;
|
||||
display: block;
|
||||
text-align: center;
|
||||
max-height: 100px;
|
||||
overflow: hidden;
|
||||
}
|
||||
ul#image-preview-list .preview img {
|
||||
max-width: 100px;
|
||||
padding: auto;
|
||||
}
|
||||
ul#image-preview-list .description {
|
||||
vertical-align: top;
|
||||
margin-left: 105px;
|
||||
}
|
||||
ul#image-preview-list .description pre {
|
||||
display: inline;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
.sheets { margin-right: 315px; }
|
||||
.sheets .col { float: left; }
|
||||
@@ -1311,7 +1355,6 @@ a.star { text-decoration: none; color: #ff8b60 }
|
||||
.sheets .btn { margin-left: 0px; margin-right: 5px; }
|
||||
.sheets .btn.right { float: right; margin-right: 3px;}
|
||||
|
||||
.stylesheet-customize-container h2 { margin-top: 15px; margin-bottom: 10px; }
|
||||
|
||||
#validation-errors {
|
||||
margin-left: 40px;
|
||||
@@ -1348,7 +1391,7 @@ a.star { text-decoration: none; color: #ff8b60 }
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#header-img-preview-container {
|
||||
#img-preview-container {
|
||||
border-width: .2em;
|
||||
border-style: dashed;
|
||||
border-color: lightgray;
|
||||
|
||||
@@ -111,7 +111,7 @@ function untoggle(execute, parent, oldtext, type) {
|
||||
if(execute) {
|
||||
var form = parent.parentNode;
|
||||
var uh = modhash; //global
|
||||
parent.innerHTML = form.executed.value;
|
||||
parent.innerHTML = form.executed.value || oldtext;
|
||||
|
||||
if(type == 'del') {
|
||||
post_form(form, type, function() {return ""});
|
||||
|
||||
@@ -164,25 +164,6 @@ function tup(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
function stylesheetSave(form, formID) {
|
||||
form.op.value = "save";
|
||||
return post_form(form, formID);
|
||||
}
|
||||
|
||||
function stylesheetPreview(formID, textboxID) {
|
||||
var form = document.getElementById(formID);
|
||||
form.op.value = "preview";
|
||||
post_form(form, formID);
|
||||
|
||||
applyStylesheetFromTextbox(textboxID);
|
||||
}
|
||||
|
||||
function applyStylesheetFromTextbox(textboxID) {
|
||||
var textbox = document.getElementById(textboxID);
|
||||
var cssText = textbox.value;
|
||||
return applyStylesheet(cssText);
|
||||
}
|
||||
|
||||
function applyStylesheet(cssText) {
|
||||
/* also referred to in the reddit.html template, for the name of the
|
||||
stylesheet set for this reddit. These must be in sync, because
|
||||
@@ -297,46 +278,100 @@ function gotoTextboxLine(textboxID, lineNo) {
|
||||
}
|
||||
}
|
||||
|
||||
function uploadHeaderImage(status) {
|
||||
var form = $('upload-header-image');
|
||||
|
||||
form.op.value = 'upload';
|
||||
$('img-status').innerHTML = status;
|
||||
show('img-status');
|
||||
function insertAtCursor(textbox, value) {
|
||||
textbox = $(textbox);
|
||||
var orig_pos = textbox.scrollTop;
|
||||
|
||||
form.submit();
|
||||
if (document.selection) { /* IE */
|
||||
textbox.focus();
|
||||
var sel = document.selection.createRange();
|
||||
sel.text = value;
|
||||
}
|
||||
else if (textbox.selectionStart) {
|
||||
var prev_start = textbox.selectionStart;
|
||||
textbox.value =
|
||||
textbox.value.substring(0, textbox.selectionStart) +
|
||||
value +
|
||||
textbox.value.substring(textbox.selectionEnd, textbox.value.length);
|
||||
prev_start += value.length;
|
||||
textbox.setSelectionRange(prev_start, prev_start);
|
||||
} else {
|
||||
textbox.value += value;
|
||||
}
|
||||
|
||||
return false;
|
||||
if(textbox.scrollHeight) {
|
||||
textbox.scrollTop = orig_pos;
|
||||
}
|
||||
|
||||
textbox.focus();
|
||||
}
|
||||
|
||||
function deleteHeaderImage(status) {
|
||||
var form = $('upload-header-image');
|
||||
|
||||
form.reset();
|
||||
form.op.value = 'delete';
|
||||
|
||||
function upload_image(form, status) {
|
||||
$('img-status').innerHTML = status;
|
||||
show('img-status');
|
||||
|
||||
form.submit();
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function completedUploadHeaderImage(status,img_src,op) {
|
||||
|
||||
function completedUploadImage(status, img_src, name, errors) {
|
||||
show('img-status');
|
||||
$('img-status').innerHTML = status;
|
||||
$('upload-header-image').reset();
|
||||
for(var i = 0; i < errors.length; i++) {
|
||||
var e = $(errors[i][0]);
|
||||
if( errors[i][1]) {
|
||||
show(e);
|
||||
e.innerHTML = errors[i][1];
|
||||
}
|
||||
else {
|
||||
hide(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$('header-img').src = img_src;
|
||||
if(img_src) {
|
||||
$('upload-image').reset();
|
||||
hide('submit-header-img');
|
||||
if (!name) {
|
||||
$('header-img').src = img_src;
|
||||
$('img-preview').src = img_src;
|
||||
show('delete-img');
|
||||
hide('submit-img');
|
||||
show('img-preview-container');
|
||||
} else {
|
||||
var img = $("img-preview_" + name);
|
||||
if(img) {
|
||||
/* Because IE isn't smart enought to eval "!img" */
|
||||
}
|
||||
else {
|
||||
var ul = $("image-preview-list");
|
||||
var li = $("img-prototype").cloneNode(true);
|
||||
li.id = "img-li_";
|
||||
ul.appendChild(li);
|
||||
re_id_node(li, ''+name);
|
||||
var name_b = $("img_name_" + name);
|
||||
if(name_b) {
|
||||
name_b.innerHTML = name;
|
||||
}
|
||||
var label = $("img_url_" + name);
|
||||
if(label) {
|
||||
label.innerHTML = "url(%%" + name + "%%)";
|
||||
}
|
||||
img = $("img-preview_" + name);
|
||||
|
||||
if(op == 'delete') {
|
||||
hide('delete-header-img');
|
||||
hide('header-img-preview-container');
|
||||
} else {
|
||||
$('header-img-preview').src = img_src;
|
||||
show('delete-header-img');
|
||||
hide('submit-header-img');
|
||||
show('header-img-preview-container');
|
||||
var sel_list = $('old-names');
|
||||
if (sel_list) {
|
||||
var opt = document.createElement('option');
|
||||
opt.innerHTML = name;
|
||||
sel_list.appendChild(opt);
|
||||
}
|
||||
}
|
||||
img.src = img_src;
|
||||
$("img-preview-a_" + name).href = img_src;
|
||||
show("img-li_" + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,7 +427,7 @@ function handleResponse(action) {
|
||||
if(r.call) {
|
||||
var calls = r.call;
|
||||
for(var i=0; i<calls.length; i++) {
|
||||
eval(calls[i]);
|
||||
eval(calls[i]);
|
||||
}
|
||||
}
|
||||
// handle shifts of focus
|
||||
@@ -448,7 +483,7 @@ function re_id_node(node, id) {
|
||||
}
|
||||
return s;
|
||||
}
|
||||
if(node.id) { node.id = add_id(node.id); }
|
||||
if(node.id && typeof(node.id) == "string") { node.id = add_id(node.id); }
|
||||
if(node.htmlFor) { add_id(node.htmlFor); }
|
||||
var children = node.childNodes;
|
||||
for(var i = 0; i < children.length; i++) {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
from r2.lib.template_helpers import static, get_domain
|
||||
from r2.models import Link, Comment, Subreddit
|
||||
%>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="${c.lang}"
|
||||
xml:lang="${c.lang}" ${c.lang_rtl and unsafe('dir="rtl"') or ''}>
|
||||
<head>
|
||||
|
||||
@@ -275,13 +275,11 @@ function update_title() {
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<form
|
||||
onsubmit="return uploadHeaderImage('${_('uploading')}');"
|
||||
name="upload-header-image" id="upload-header-image"
|
||||
enctype="multipart/form-data"
|
||||
class="pretty-form"
|
||||
target="upload-header-iframe"
|
||||
action="/api/upload_header_img" method="post">
|
||||
<form id="upload-image"
|
||||
enctype="multipart/form-data"
|
||||
class="pretty-form"
|
||||
target="upload-iframe"
|
||||
action="/api/upload_sr_img" method="post">
|
||||
<table class="preftable">
|
||||
%if thing.site and thing.site.can_change_stylesheet(c.user) and not g.css_killswitch:
|
||||
<tr>
|
||||
@@ -299,33 +297,32 @@ function update_title() {
|
||||
<div class="spacer">
|
||||
<label for="headerfile">upload header image</label>
|
||||
<input type="file" name="file" id="headerfile"
|
||||
onchange="show('submit-header-img')"/>
|
||||
<button id="submit-header-img"
|
||||
type="button"
|
||||
onclick="return uploadHeaderImage('${_('uploading')}');"
|
||||
style="display: none;"
|
||||
>${_("Upload")}
|
||||
</button>
|
||||
<button id="delete-header-img"
|
||||
class="btn"
|
||||
onclick="return deleteHeaderImage('${_('deleting')}')"
|
||||
%if not thing.site.header:
|
||||
style="display: none;"
|
||||
%endif
|
||||
>
|
||||
${_('restore default')}
|
||||
onchange="show('submit-img')"/>
|
||||
<button id="submit-img" type="submit" name="upload"
|
||||
onclick="return upload_image(this.form, '${_('uploading')}');"
|
||||
style="display: none;" >
|
||||
${_('Upload')}
|
||||
</button>
|
||||
|
||||
<button id="delete-img"
|
||||
%if not thing.site.header:
|
||||
style="display: none;"
|
||||
%endif
|
||||
onclick="return post_form(this.form, 'delete_sr_header');">
|
||||
${_('restore default header')}
|
||||
</button>
|
||||
|
||||
<input type="hidden" name="uh" value="${c.modhash}" />
|
||||
<input type="hidden" name="r" value="${c.site.name}" />
|
||||
<input type="hidden" name="op" value="upload" />
|
||||
<input type="hidden" name="header" value="1" />
|
||||
|
||||
<span style="display: none;" class="error" id="img-status"></span>
|
||||
<iframe src="about:blank"
|
||||
<iframe src="about:blank"
|
||||
width="600" height="200" style="display: none;"
|
||||
name="upload-header-iframe" id="upload-header-iframe"></iframe>
|
||||
name="upload-iframe" id="upload-iframe"></iframe>
|
||||
</div>
|
||||
<div id="header-img-preview-container" style="display: none;">
|
||||
<img id="header-img-preview" alt="header preview"
|
||||
<div id="img-preview-container" style="display: none;">
|
||||
<img id="img-preview" alt="header preview"
|
||||
%if thing.site.header:
|
||||
src="${thing.site.header}"
|
||||
%else:
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<%
|
||||
com_label = ungettext("comment", "comments", thing.num_comments)
|
||||
url = add_sr(thing.permalink, force_hostname = True)
|
||||
use_thumbs = thing.thumbnail and not request.GET.has_key("nothumbs")
|
||||
%>
|
||||
<item>
|
||||
<title>${thing.title}</title>
|
||||
@@ -36,7 +37,7 @@
|
||||
<pubDate>${thing._date.isoformat()}</pubDate>
|
||||
<dc:date>${thing._date.isoformat()}</dc:date>
|
||||
<description>
|
||||
%if thing.thumbnail:
|
||||
%if use_thumbs:
|
||||
<table>
|
||||
<tr><td>
|
||||
<a href="${url}"><img src="${thing.thumbnail}" alt="${thing.title}" title="${thing.title}" /></a>
|
||||
@@ -51,11 +52,11 @@
|
||||
<br/>
|
||||
<a href="${thing.url}">[link]</a>
|
||||
<a href="${url}">[${thing.num_comments} ${com_label}]</a>
|
||||
%if thing.thumbnail:
|
||||
%if use_thumbs:
|
||||
</td></tr></table>
|
||||
%endif
|
||||
</description>
|
||||
%if thing.thumbnail:
|
||||
%if use_thumbs:
|
||||
<media:title>${thing.title}</media:title>
|
||||
<media:thumbnail url="${thing.thumbnail}" />
|
||||
%endif
|
||||
|
||||
@@ -216,7 +216,7 @@ ${self.RenderPrintable()}
|
||||
%>
|
||||
<%def name="_link()" buffered="True">
|
||||
<a href="/" class="${a_class}" onclick="${onclick}"
|
||||
id="${tag.replace('/','_')}_a">${title}</a>
|
||||
id="a_${tag.replace('/','_')}">${title}</a>
|
||||
</%def>
|
||||
<%
|
||||
link = _link()
|
||||
|
||||
@@ -30,7 +30,7 @@ http://${g.domain}/goto?share=true&id=${thing.link._fullname}
|
||||
|
||||
<% from r2.lib.strings import strings, plurals %>${ungettext("There is currently %(num_comments)s on this link. You can view it here:", "There are currently %(num_comments)s on this link. You can view them here:", thing.link.num_comments) % dict(num_comments = strings.number_label % (thing.link.num_comments, plurals.N_comments(thing.link.num_comments)))}
|
||||
|
||||
<% from r2.lib.template_helpers import add_sr %>http://${g.domain}${add_sr(thing.link.make_permalink_slow())}
|
||||
<% from r2.lib.template_helpers import add_sr %>${add_sr(thing.link.make_permalink_slow(), force_hostname = True)}
|
||||
|
||||
___
|
||||
If you would not like to receive emails from reddit.com in the future, visit http://${g.domain}/mail/optout?x=${thing.msg_hash}
|
||||
@@ -25,17 +25,19 @@
|
||||
|
||||
%>
|
||||
<%namespace file="utils.html" import="error_field, language_tool"/>
|
||||
<%namespace file="printable.html" import="yes_no_button"/>
|
||||
|
||||
<div class="stylesheet-customize-container">
|
||||
|
||||
<form
|
||||
onsubmit="return stylesheetSave(this, 'subreddit_stylesheet')"
|
||||
name="subreddit_stylesheet" id="subreddit_stylesheet"
|
||||
class="pretty-form sr-form"
|
||||
action="/post/subreddit_stylesheet" method="post" >
|
||||
|
||||
onsubmit="return post_form(this, 'subreddit_stylesheet')"
|
||||
name="subreddit_stylesheet" id="subreddit_stylesheet"
|
||||
class="pretty-form sr-form"
|
||||
action="/post/subreddit_stylesheet" method="post" >
|
||||
|
||||
<input type="hidden" name="r" value="${thing.site.name}" />
|
||||
<input type="hidden" name="op" value="save" />
|
||||
<input type="hidden" name="op" value="" />
|
||||
<input type="hidden" name="thumbbucket"
|
||||
value="http:/${g.s3_thumb_bucket}${c.site._fullname}" />
|
||||
|
||||
<h2>${_("stylesheet")}</h2>
|
||||
<div class="sheets">
|
||||
@@ -77,10 +79,14 @@
|
||||
onclick="return hideDefaultStylesheet();">
|
||||
${_('hide the default stylesheet')}
|
||||
</button>
|
||||
<button class="btn" name="save" type="submit">${_("save")}</button>
|
||||
<button class="btn" name="preview" type="button"
|
||||
onclick="stylesheetPreview('subreddit_stylesheet','stylesheet_contents');">${_("preview")}</button>
|
||||
|
||||
<button class="btn" name="save" type="submit"
|
||||
onclick="this.form.op.value='save'; return true;">
|
||||
${_('save')}
|
||||
</button>
|
||||
<button class="btn" name="preview" type="submit"
|
||||
onclick="this.form.op.value='preview'; return true;">
|
||||
${_('preview')}
|
||||
</button>
|
||||
<span id="status" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,10 +96,157 @@
|
||||
<!-- populated from AJAX requests to /api/subreddit_stylesheet -->
|
||||
</ul>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
|
||||
</form>
|
||||
|
||||
<div id="images">
|
||||
<h2><a name="images">${_("images")}</a></h2>
|
||||
|
||||
<form class="pretty-form image-upload" enctype="multipart/form-data"
|
||||
target="upload-iframe" id="upload-image"
|
||||
action="/api/upload_sr_img" method="post"
|
||||
onsubmit="return check_name(this)">
|
||||
<input type="hidden" name="uh" value="${c.modhash}" />
|
||||
<input type="hidden" name="r" value="${c.site.name}" />
|
||||
<table>
|
||||
<tr>
|
||||
<th>
|
||||
<label for="srfile">${_("image file:")}</label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="file" name="file" id="srfile"
|
||||
onchange="return file_changed(this)"/>
|
||||
<button id="submit-header-img" type="submit" name="upload"
|
||||
onclick="return upload_image(this.form, '${_('uploading')}');"
|
||||
style="display: none;" >
|
||||
${_('Upload')}
|
||||
</button>
|
||||
<span style="display: none;" class="error" id="img-status"></span>
|
||||
${error_field("IMAGE_ERROR", "span")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<label for="img-name">${_("new image name:")}</label>
|
||||
</th>
|
||||
<td>
|
||||
<input id="img-name" name="name" value="" type="text"/>
|
||||
${error_field("BAD_CSS_NAME", "span")}
|
||||
<br/>
|
||||
<span class="little gray">
|
||||
${_("(image names should consist of alphanumeric characters and '-' only)")}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p class="error">
|
||||
${_("Note: any changes to images here will be reflected immediately on reload and cannot be undone.")}
|
||||
</p>
|
||||
</form>
|
||||
<script type="text/javascript">
|
||||
/* <![CDATA[ */
|
||||
function too_big() {
|
||||
$("IMAGE_ERROR").innerHTML = "image too big: there is a 500k cap";
|
||||
$("img-status").innerHTML = "";
|
||||
}
|
||||
function get_name(source) {
|
||||
if(source.id && source.id.split) {
|
||||
var name = source.id.split('_');
|
||||
return name[name.length - 1];
|
||||
}
|
||||
}
|
||||
function paste_url(source) {
|
||||
insertAtCursor("stylesheet_contents",
|
||||
"url(%%" + get_name(source) + "%%)");
|
||||
return false;
|
||||
}
|
||||
function delete_img(button) {
|
||||
var n=get_name(button);
|
||||
var form = button.parentNode.parentNode;
|
||||
if (n && form && form.id && typeof(form.id) != "string") {
|
||||
form.id.value = n;
|
||||
form.img_name.value = n;
|
||||
}
|
||||
return deletetoggle(button, 'delete_sr_img');
|
||||
}
|
||||
function file_changed(file_input) {
|
||||
show('submit-header-img');
|
||||
$("img-status").innerHTML = "";
|
||||
if(file_input.value) {
|
||||
var f = file_input.value.replace(/.*[\/\\]/, "").split('.')[0];
|
||||
f = f.replace(/[ _]/g, "-");
|
||||
if(! $('img-name').value)
|
||||
$('img-name').value = f;
|
||||
}
|
||||
}
|
||||
function check_name(form) {
|
||||
var name = form.name.value;
|
||||
var r = new RegExp(/[^a-zA-Z0-9\-]/);
|
||||
if (name == "" || name.match(r)) {
|
||||
$("img-status").innerHTML = "";
|
||||
show("BAD_CSS_NAME");
|
||||
$("BAD_CSS_NAME").innerHTML = "${_('bad image name')}";
|
||||
return false;
|
||||
}
|
||||
else if(form.srfile.value) {
|
||||
$("BAD_CSS_NAME").innerHTML = "";
|
||||
$("IMAGE_ERROR").innerHTML = "";
|
||||
return true;
|
||||
} else {
|
||||
$("img-status").innerHTML = "";
|
||||
show("IMAGE_ERROR");
|
||||
$("IMAGE_ERROR").innerHTML = "${_('please select an image')}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/* ]]> */
|
||||
</script>
|
||||
<ul id="image-preview-list" class="image-list">
|
||||
<%def name="make_li(name='', img = None, prototype=False)">
|
||||
<li ${"style='display:none'" if img is None else ""}
|
||||
id="${'img-prototype' if prototype else ('img-li_%s' % name) }">
|
||||
<%
|
||||
if img is not None:
|
||||
img = "http:/%s%s_%d.png" % (g.s3_thumb_bucket, c.site._fullname, img)
|
||||
else:
|
||||
img = "/static/kill.png"
|
||||
%>
|
||||
<a href="${img}" id="img-preview-a_${name}" class="preview">
|
||||
<img id="img-preview_${name}" src="${img}" alt="Image ${name}"
|
||||
title="click to preview"/>
|
||||
</a>
|
||||
<div class="description">
|
||||
<b id="img_name_${name}">
|
||||
${name}
|
||||
</b>
|
||||
<br/>
|
||||
<span id="description_${name}">link:</span>
|
||||
<pre id="img_url_${name}">url(%%${name}%%)</pre>
|
||||
<br/>
|
||||
<a id="paste_image_${name}" href="javascript:void(0)"
|
||||
onclick="return paste_url(this)">
|
||||
${_("paste into stylesheet")}
|
||||
</a>
|
||||
<br/>
|
||||
${yes_no_button("delete_sr_img", name,
|
||||
_("delete this image"),
|
||||
"return delete_img(this)",
|
||||
"", img_name = name)}
|
||||
</div>
|
||||
</li>
|
||||
</%def>
|
||||
${make_li(prototype=True)}
|
||||
%for name, img_num in c.site.get_images():
|
||||
${make_li(name = name, img = img_num)}
|
||||
%endfor
|
||||
</ul>
|
||||
|
||||
<iframe src="about:blank" width="600" height="200" style="display: none;"
|
||||
name="upload-iframe" id="upload-iframe"></iframe>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="clear:left"><!--IEsux--></div>
|
||||
<div id="preview-table" style="display:none">
|
||||
<h2><a name="preview">${_("preview")}</a></h2>
|
||||
<table>
|
||||
|
||||
@@ -19,10 +19,16 @@
|
||||
## All portions of the code written by CondeNet are Copyright (c) 2006-2008
|
||||
## CondeNet, Inc. All Rights Reserved.
|
||||
################################################################################
|
||||
<%!
|
||||
import simplejson
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
parent.completedUploadHeaderImage('${thing.status}','${thing.img_src}', '${thing.op}');
|
||||
<%
|
||||
errors = simplejson.dumps(thing.errors)
|
||||
%>
|
||||
parent.completedUploadImage('${thing.status}','${thing.img_src or ""}', '${thing.name or ""}', ${unsafe(errors)});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user