added the ability for subreddit moderators to upload custom images for use in their custom stylesheets

This commit is contained in:
KeyserSosa
2008-09-16 15:11:21 -07:00
parent 363a306cb2
commit 7a30629bf7
20 changed files with 570 additions and 134 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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(),

View File

@@ -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):

View File

@@ -42,6 +42,7 @@ class Globals(object):
'max_comments',
'num_side_reddits',
'num_query_queue_workers',
'max_sr_images',
]
bool_props = ['debug', 'translator',

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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):

View File

@@ -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:

View File

@@ -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 = ''

View File

@@ -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;

View File

@@ -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 ""});

View File

@@ -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++) {

View File

@@ -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>

View File

@@ -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:

View File

@@ -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:
&lt;table&gt;
&lt;tr&gt;&lt;td&gt;
&lt;a href="${url}"&gt;&lt;img src="${thing.thumbnail}" alt="${thing.title}" title="${thing.title}" /&gt;&lt;/a&gt;
@@ -51,11 +52,11 @@
&lt;br/&gt;
&lt;a href="${thing.url}"&gt;[link]&lt;/a&gt;
&lt;a href="${url}"&gt;[${thing.num_comments} ${com_label}]&lt;/a&gt;
%if thing.thumbnail:
%if use_thumbs:
&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
%endif
</description>
%if thing.thumbnail:
%if use_thumbs:
<media:title>${thing.title}</media:title>
<media:thumbnail url="${thing.thumbnail}" />
%endif

View File

@@ -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()

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>