mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-04-27 03:00:12 -04:00
Use s3 direct POST for ads images.
This commit is contained in:
@@ -386,12 +386,12 @@ def make_map(config):
|
||||
type='click')
|
||||
mc('/api/gadget/:type', controller='api', action='gadget')
|
||||
mc('/api/:action', controller='promoteapi',
|
||||
requirements=dict(action=("promote|unpromote|edit_promo|link_thumb|"
|
||||
"freebie|promote_note|update_pay|"
|
||||
requirements=dict(action=("promote|unpromote|edit_promo|ad_s3_callback|"
|
||||
"ad_s3_params|freebie|promote_note|update_pay|"
|
||||
"edit_campaign|delete_campaign|"
|
||||
"add_roadblock|rm_roadblock|check_inventory|"
|
||||
"refund_campaign|terminate_campaign|"
|
||||
"review_fraud|create_promo|link_mobile_ad_image|"
|
||||
"review_fraud|create_promo|"
|
||||
"toggle_pause_campaign")))
|
||||
mc('/api/:action', controller='apiminimal',
|
||||
requirements=dict(action="new_captcha"))
|
||||
|
||||
@@ -24,8 +24,12 @@ from datetime import datetime, timedelta
|
||||
|
||||
from babel.dates import format_date
|
||||
from babel.numbers import format_number
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import urllib
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
from pylons import request
|
||||
from pylons import tmpl_context as c
|
||||
@@ -41,17 +45,20 @@ from r2.lib.authorize import (
|
||||
PROFILE_LIMIT,
|
||||
)
|
||||
from r2.lib.authorize.api import AuthorizeNetException
|
||||
from r2.lib import hooks, inventory, promote
|
||||
from r2.lib import (
|
||||
hooks,
|
||||
inventory,
|
||||
media,
|
||||
promote,
|
||||
s3_helpers,
|
||||
)
|
||||
from r2.lib.base import abort
|
||||
from r2.lib.db import queries
|
||||
from r2.lib.errors import errors
|
||||
from r2.lib.filters import websafe
|
||||
from r2.lib.template_helpers import format_html
|
||||
from r2.lib.media import (
|
||||
force_mobile_ad_image,
|
||||
force_thumbnail,
|
||||
thumbnail_url,
|
||||
_scrape_media,
|
||||
from r2.lib.filters import jssafe, scriptsafe_dumps
|
||||
from r2.lib.template_helpers import (
|
||||
add_sr,
|
||||
format_html,
|
||||
)
|
||||
from r2.lib.memoize import memoize
|
||||
from r2.lib.menus import NamedButton, NavButton, NavMenu, QueryButton
|
||||
@@ -68,11 +75,11 @@ from r2.lib.pages import (
|
||||
RenderableCampaign,
|
||||
Roadblocks,
|
||||
SponsorLookupUser,
|
||||
UploadedImage,
|
||||
)
|
||||
from r2.lib.pages.things import default_thing_wrapper, wrap_links
|
||||
from r2.lib.system_messages import user_added_messages
|
||||
from r2.lib.utils import (
|
||||
constant_time_compare,
|
||||
is_subdomain,
|
||||
to_date,
|
||||
to36,
|
||||
@@ -105,6 +112,7 @@ from r2.lib.validator import (
|
||||
VModhash,
|
||||
VOneOf,
|
||||
VOSVersion,
|
||||
VPrintable,
|
||||
VPriority,
|
||||
VPromoCampaign,
|
||||
VPromoTarget,
|
||||
@@ -143,6 +151,35 @@ ANDROID_DEVICES = ('phone', 'tablet',)
|
||||
|
||||
ADZERK_URL_MAX_LENGTH = 499
|
||||
|
||||
EXPIRES_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
||||
ALLOWED_IMAGE_TYPES = set(["image/jpg", "image/jpeg", "image/png"])
|
||||
|
||||
def _format_expires(expires):
|
||||
return expires.strftime(EXPIRES_DATE_FORMAT)
|
||||
|
||||
|
||||
def _get_callback_hmac(username, key, expires):
|
||||
secret = g.secrets["s3_direct_post_callback"]
|
||||
expires_str = _format_expires(expires)
|
||||
data = "|".join([username, key, expires_str])
|
||||
|
||||
return hmac.new(secret, data, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def _force_images(link, thumbnail, mobile):
|
||||
changed = False
|
||||
|
||||
if thumbnail:
|
||||
media.force_thumbnail(link, thumbnail["data"], thumbnail["ext"])
|
||||
changed = True
|
||||
|
||||
if mobile:
|
||||
media.force_mobile_ad_image(link, mobile["data"], mobile["ext"])
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def campaign_has_oversold_error(form, campaign):
|
||||
if campaign.priority.inventory_override:
|
||||
return
|
||||
@@ -175,11 +212,71 @@ def has_oversold_error(form, campaign, start, end, bid, cpm, target, location):
|
||||
return True
|
||||
|
||||
|
||||
def _key_to_dict(key, data=False):
|
||||
timer = g.stats.get_timer("providers.s3.get_ads_key_meta.with_%s" %
|
||||
("data" if data else "no_data"))
|
||||
timer.start()
|
||||
|
||||
url = key.generate_url(expires_in=0, query_auth=False)
|
||||
# Generating an S3 url without authentication fails for IAM roles.
|
||||
# This removes the bad query params.
|
||||
# see: https://github.com/boto/boto/issues/2043
|
||||
url = promote.update_query(url, {"x-amz-security-token": None}, unset=True)
|
||||
|
||||
result = {
|
||||
"url": url,
|
||||
"data": key.get_contents_as_string() if data else None,
|
||||
"ext": key.get_metadata("ext"),
|
||||
}
|
||||
|
||||
timer.stop()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _get_ads_keyspace(thing):
|
||||
return "ads/%s/" % thing._fullname
|
||||
|
||||
|
||||
def _get_ads_images(thing, data=False, **kwargs):
|
||||
images = {}
|
||||
|
||||
timer = g.stats.get_timer("providers.s3.get_ads_image_keys")
|
||||
timer.start()
|
||||
|
||||
keys = s3_helpers.get_keys(g.s3_client_uploads_bucket, prefix=_get_ads_keyspace(thing), **kwargs)
|
||||
|
||||
timer.stop()
|
||||
|
||||
for key in keys:
|
||||
filename = os.path.basename(key.key)
|
||||
name, ext = os.path.splitext(filename)
|
||||
|
||||
if name not in ("mobile", "thumbnail"):
|
||||
continue
|
||||
|
||||
images[name] = _key_to_dict(key, data=data)
|
||||
|
||||
return images
|
||||
|
||||
|
||||
def _clear_ads_images(thing):
|
||||
timer = g.stats.get_timer("providers.s3.delete_ads_image_keys")
|
||||
timer.start()
|
||||
|
||||
s3_helpers.delete_keys(g.s3_client_uploads_bucket, prefix=_get_ads_keyspace(thing))
|
||||
|
||||
timer.stop()
|
||||
|
||||
|
||||
class PromoteController(RedditController):
|
||||
@validate(VSponsor())
|
||||
def GET_new_promo(self):
|
||||
ads_images = _get_ads_images(c.user)
|
||||
images = {k: v.get("url") for k, v in ads_images.iteritems()}
|
||||
|
||||
return PromotePage(title=_("create sponsored link"),
|
||||
content=PromoteLinkNew(),
|
||||
content=PromoteLinkNew(images),
|
||||
extra_js_config={
|
||||
"ads_virtual_page": "new-promo",
|
||||
}).render()
|
||||
@@ -672,7 +769,7 @@ class PromoteApiController(ApiController):
|
||||
else:
|
||||
form.set_text('.status', _('refund not needed'))
|
||||
|
||||
@validatedMultipartForm(
|
||||
@validatedForm(
|
||||
VSponsor('link_id36'),
|
||||
VModhash(),
|
||||
VRatelimit(rate_user=True,
|
||||
@@ -697,20 +794,25 @@ class PromoteApiController(ApiController):
|
||||
third_party_tracking=VUrl("third_party_tracking"),
|
||||
third_party_tracking_2=VUrl("third_party_tracking_2"),
|
||||
is_managed=VBoolean("is_managed"),
|
||||
thumbnail_file=VUploadLength('file', 500*1024),
|
||||
)
|
||||
def POST_create_promo(self, form, jquery, username, title, url,
|
||||
selftext, kind, disable_comments, sendreplies,
|
||||
media_url, media_autoplay, media_override,
|
||||
iframe_embed_url, media_url_type, domain_override,
|
||||
third_party_tracking, third_party_tracking_2,
|
||||
is_managed, thumbnail_file):
|
||||
return self._edit_promo(form, jquery, username, title, url,
|
||||
selftext, kind, disable_comments, sendreplies,
|
||||
media_url, media_autoplay, media_override,
|
||||
iframe_embed_url, media_url_type, domain_override,
|
||||
third_party_tracking, third_party_tracking_2,
|
||||
is_managed, thumbnail_file=thumbnail_file)
|
||||
is_managed):
|
||||
|
||||
images = _get_ads_images(c.user, data=True, meta=True)
|
||||
|
||||
return self._edit_promo(
|
||||
form, jquery, username, title, url,
|
||||
selftext, kind, disable_comments, sendreplies,
|
||||
media_url, media_autoplay, media_override,
|
||||
iframe_embed_url, media_url_type, domain_override,
|
||||
third_party_tracking, third_party_tracking_2, is_managed,
|
||||
thumbnail=images.get("thumbnail", None),
|
||||
mobile=images.get("mobile", None),
|
||||
)
|
||||
|
||||
@validatedForm(
|
||||
VSponsor('link_id36'),
|
||||
@@ -745,22 +847,30 @@ class PromoteApiController(ApiController):
|
||||
iframe_embed_url, media_url_type, domain_override,
|
||||
third_party_tracking, third_party_tracking_2,
|
||||
is_managed, l):
|
||||
return self._edit_promo(form, jquery, username, title, url,
|
||||
selftext, kind, disable_comments, sendreplies,
|
||||
media_url, media_autoplay, media_override,
|
||||
iframe_embed_url, media_url_type, domain_override,
|
||||
third_party_tracking, third_party_tracking_2,
|
||||
is_managed, l=l)
|
||||
|
||||
images = _get_ads_images(l, data=True, meta=True)
|
||||
|
||||
return self._edit_promo(
|
||||
form, jquery, username, title, url,
|
||||
selftext, kind, disable_comments, sendreplies,
|
||||
media_url, media_autoplay, media_override,
|
||||
iframe_embed_url, media_url_type, domain_override,
|
||||
third_party_tracking, third_party_tracking_2, is_managed,
|
||||
l=l,
|
||||
thumbnail=images.get("thumbnail", None),
|
||||
mobile=images.get("mobile", None),
|
||||
)
|
||||
|
||||
def _edit_promo(self, form, jquery, username, title, url,
|
||||
selftext, kind, disable_comments, sendreplies,
|
||||
media_url, media_autoplay, media_override,
|
||||
iframe_embed_url, media_url_type, domain_override,
|
||||
third_party_tracking, third_party_tracking_2,
|
||||
is_managed, l=None, thumbnail_file=None):
|
||||
is_managed, l=None, thumbnail=None, mobile=None):
|
||||
should_ratelimit = False
|
||||
is_self = (kind == "self")
|
||||
is_link = not is_self
|
||||
is_new_promoted = not l
|
||||
if not c.user_is_sponsor:
|
||||
should_ratelimit = True
|
||||
|
||||
@@ -768,7 +878,7 @@ class PromoteApiController(ApiController):
|
||||
c.errors.remove((errors.RATELIMIT, 'ratelimit'))
|
||||
|
||||
# check for user override
|
||||
if not l and c.user_is_sponsor and username:
|
||||
if is_new_promoted and c.user_is_sponsor and username:
|
||||
try:
|
||||
user = Account._by_name(username)
|
||||
except NotFound:
|
||||
@@ -814,7 +924,7 @@ class PromoteApiController(ApiController):
|
||||
return
|
||||
|
||||
# users can change the disable_comments on promoted links
|
||||
if ((not l or not promote.is_promoted(l)) and
|
||||
if ((is_new_promoted or not promote.is_promoted(l)) and
|
||||
(form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG) or
|
||||
jquery.has_errors('ratelimit', errors.RATELIMIT))):
|
||||
return
|
||||
@@ -822,7 +932,7 @@ class PromoteApiController(ApiController):
|
||||
if is_self and form.has_errors('text', errors.TOO_LONG):
|
||||
return
|
||||
|
||||
if not l:
|
||||
if is_new_promoted:
|
||||
# creating a new promoted link
|
||||
l = promote.new_promotion(
|
||||
is_self=is_self,
|
||||
@@ -839,13 +949,7 @@ class PromoteApiController(ApiController):
|
||||
l.third_party_tracking_2 = third_party_tracking_2 or None
|
||||
l._commit()
|
||||
|
||||
# only set the thumbnail when creating a link
|
||||
if thumbnail_file:
|
||||
try:
|
||||
force_thumbnail(l, thumbnail_file)
|
||||
l._commit()
|
||||
except IOError:
|
||||
pass
|
||||
_force_images(l, thumbnail=thumbnail, mobile=mobile)
|
||||
|
||||
form.redirect(promote.promo_edit_url(l))
|
||||
|
||||
@@ -860,6 +964,9 @@ class PromoteApiController(ApiController):
|
||||
l.title = title
|
||||
changed = True
|
||||
|
||||
if _force_images(l, thumbnail=thumbnail, mobile=mobile):
|
||||
changed = True
|
||||
|
||||
# type changing
|
||||
if is_self != l.is_self:
|
||||
l.set_content(is_self, selftext if is_self else url)
|
||||
@@ -888,13 +995,13 @@ class PromoteApiController(ApiController):
|
||||
|
||||
if c.user_is_sponsor and scraper_embed and media_url != l.media_url:
|
||||
if media_url:
|
||||
media = _scrape_media(
|
||||
scraped = media._scrape_media(
|
||||
media_url, autoplay=media_autoplay,
|
||||
save_thumbnail=False, use_cache=True)
|
||||
|
||||
if media:
|
||||
l.set_media_object(media.media_object)
|
||||
l.set_secure_media_object(media.secure_media_object)
|
||||
if scraped:
|
||||
l.set_media_object(scraped.media_object)
|
||||
l.set_secure_media_object(scraped.secure_media_object)
|
||||
l.media_url = media_url
|
||||
l.gifts_embed_url = None
|
||||
l.media_autoplay = media_autoplay
|
||||
@@ -957,6 +1064,11 @@ class PromoteApiController(ApiController):
|
||||
l.managed_promo = is_managed
|
||||
|
||||
l._commit()
|
||||
|
||||
# clean up so the same images don't reappear if they create
|
||||
# another link
|
||||
_clear_ads_images(thing=c.user if is_new_promoted else l)
|
||||
|
||||
form.redirect(promote.promo_edit_url(l))
|
||||
|
||||
@validatedForm(
|
||||
@@ -1361,36 +1473,77 @@ class PromoteApiController(ApiController):
|
||||
else:
|
||||
_handle_failed_payment()
|
||||
|
||||
@validate(
|
||||
VSponsor("link_name"),
|
||||
@json_validate(
|
||||
VSponsor("link"),
|
||||
VModhash(),
|
||||
link=VByName('link_name'),
|
||||
file=VUploadLength('file', 500*1024),
|
||||
img_type=VImageType('img_type'),
|
||||
link=VLink("link"),
|
||||
kind=VOneOf("kind", ["thumbnail", "mobile"]),
|
||||
filepath=nop("filepath"),
|
||||
ajax=VBoolean("ajax", default=True)
|
||||
)
|
||||
def POST_link_thumb(self, link=None, file=None, img_type='jpg'):
|
||||
if not link or (promote.is_promoted(link) and not c.user_is_sponsor):
|
||||
# only let sponsors edit thumbnails of live promos
|
||||
return abort(403, 'forbidden')
|
||||
def POST_ad_s3_params(self, responder, link, kind, filepath, ajax):
|
||||
filename, ext = os.path.splitext(filepath)
|
||||
mime_type, encoding = mimetypes.guess_type(filepath)
|
||||
|
||||
force_thumbnail(link, file, file_type=".%s" % img_type)
|
||||
link._commit()
|
||||
return UploadedImage(_('saved'), thumbnail_url(link), "", errors=errors,
|
||||
form_id="image-upload").render()
|
||||
if not mime_type or mime_type not in ALLOWED_IMAGE_TYPES:
|
||||
request.environ["extra_error_data"] = {
|
||||
"message": _("image must be a jpg or png"),
|
||||
}
|
||||
abort(403)
|
||||
|
||||
keyspace = _get_ads_keyspace(link if link else c.user)
|
||||
key = os.path.join(keyspace, kind)
|
||||
redirect = None
|
||||
|
||||
if not ajax:
|
||||
now = datetime.now().replace(tzinfo=g.tz)
|
||||
signature = _get_callback_hmac(
|
||||
username=c.user.name,
|
||||
key=key,
|
||||
expires=now,
|
||||
)
|
||||
path = ("/api/ad_s3_callback?hmac=%s&ts=%s" %
|
||||
(signature, _format_expires(now)))
|
||||
redirect = add_sr(path, sr_path=False)
|
||||
|
||||
return s3_helpers.get_post_args(
|
||||
bucket=g.s3_client_uploads_bucket,
|
||||
key=key,
|
||||
success_action_redirect=redirect,
|
||||
success_action_status="201",
|
||||
content_type=mime_type,
|
||||
meta={
|
||||
"x-amz-meta-ext": ext,
|
||||
},
|
||||
)
|
||||
|
||||
@validate(
|
||||
VSponsor("link_name"),
|
||||
VModhash(),
|
||||
link=VByName('link_name'),
|
||||
file=VUploadLength('file', 500*1024),
|
||||
img_type=VImageType('img_type'),
|
||||
VSponsor(),
|
||||
expires=VDate("ts", format=EXPIRES_DATE_FORMAT),
|
||||
signature=VPrintable("hmac", 255),
|
||||
callback=nop("callback"),
|
||||
key=nop("key"),
|
||||
)
|
||||
def POST_link_mobile_ad_image(self, link=None, file=None, img_type='jpg'):
|
||||
if not (link and c.user_is_sponsor and file):
|
||||
# only sponsors can set the mobile img
|
||||
return abort(403, 'forbidden')
|
||||
def GET_ad_s3_callback(self, expires, signature, callback, key):
|
||||
now = datetime.now(tz=g.tz)
|
||||
if (expires + timedelta(minutes=10) < now):
|
||||
self.abort404()
|
||||
|
||||
force_mobile_ad_image(link, file, file_type=".%s" % img_type)
|
||||
link._commit()
|
||||
return UploadedImage(_('saved'), link.mobile_ad_url, "", errors=errors,
|
||||
form_id="mobile-ad-image-upload").render()
|
||||
expected_mac = _get_callback_hmac(
|
||||
username=c.user.name,
|
||||
key=key,
|
||||
expires=expires,
|
||||
)
|
||||
|
||||
if not constant_time_compare(signature, expected_mac):
|
||||
self.abort404()
|
||||
|
||||
template = "<script>parent.__s3_callbacks__[%(callback)s](%(data)s);</script>"
|
||||
image = _key_to_dict(
|
||||
s3_helpers.get_key(g.s3_client_uploads_bucket, key))
|
||||
response = {
|
||||
"callback": scriptsafe_dumps(callback),
|
||||
"data": scriptsafe_dumps(image),
|
||||
}
|
||||
|
||||
return format_html(template, response)
|
||||
|
||||
@@ -547,6 +547,7 @@ module["sponsored"] = LocalizedModule("sponsored.js",
|
||||
"lib/ui.core.js",
|
||||
"lib/ui.datepicker.js",
|
||||
"lib/react-with-addons-0.11.2.js",
|
||||
"image-upload.js",
|
||||
"sponsored.js"
|
||||
)
|
||||
|
||||
|
||||
@@ -4356,7 +4356,10 @@ class PromoteLinkBase(Templated):
|
||||
|
||||
|
||||
class PromoteLinkNew(PromoteLinkBase):
|
||||
pass
|
||||
def __init__(self, images=None, *a, **kw):
|
||||
images = images or {}
|
||||
self.images = images
|
||||
PromoteLinkBase.__init__(self, **kw)
|
||||
|
||||
|
||||
class PromoteLinkEdit(PromoteLinkBase):
|
||||
|
||||
@@ -180,10 +180,14 @@ def is_pending(campaign):
|
||||
today = promo_datetime_now().date()
|
||||
return today < to_date(campaign.start_date)
|
||||
|
||||
def update_query(base_url, query_updates):
|
||||
def update_query(base_url, query_updates, unset=False):
|
||||
scheme, netloc, path, params, query, fragment = urlparse.urlparse(base_url)
|
||||
query_dict = urlparse.parse_qs(query)
|
||||
query_dict.update(query_updates)
|
||||
|
||||
if unset:
|
||||
query_dict = dict((k, v) for k, v in query_dict.iteritems() if v is not None)
|
||||
|
||||
query = urllib.urlencode(query_dict, doseq=True)
|
||||
return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
|
||||
|
||||
|
||||
@@ -20,10 +20,17 @@
|
||||
# Inc. All Rights Reserved.
|
||||
###############################################################################
|
||||
|
||||
import base64
|
||||
import boto
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from pylons import app_globals as g
|
||||
|
||||
from boto.s3.key import Key
|
||||
|
||||
HADOOP_FOLDER_SUFFIX = '_$folder$'
|
||||
|
||||
@@ -62,7 +69,7 @@ def get_text_from_s3(s3_connection, path):
|
||||
"""Read a file from S3 and return it as text."""
|
||||
bucket_name, key_name = _from_path(path)
|
||||
bucket = s3_connection.get_bucket(bucket_name)
|
||||
k = Key(bucket)
|
||||
k = boto.s3.Key(bucket)
|
||||
k.key = key_name
|
||||
txt = k.get_contents_as_string()
|
||||
return txt
|
||||
@@ -74,7 +81,7 @@ def mv_file_s3(s3_connection, src_path, dst_path):
|
||||
dst_bucket_name, dst_key_name = _from_path(dst_path)
|
||||
|
||||
src_bucket = s3_connection.get_bucket(src_bucket_name)
|
||||
k = Key(src_bucket)
|
||||
k = boto.s3.Key(src_bucket)
|
||||
k.key = src_key_name
|
||||
k.copy(dst_bucket_name, dst_key_name)
|
||||
k.delete()
|
||||
@@ -100,7 +107,7 @@ def copy_to_s3(s3_connection, local_path, dst_path, verbose=False):
|
||||
return
|
||||
|
||||
key_name = os.path.join(dst_key_name, filename)
|
||||
k = Key(bucket)
|
||||
k = boto.s3.Key(bucket)
|
||||
k.key = key_name
|
||||
|
||||
kw = {}
|
||||
@@ -109,3 +116,187 @@ def copy_to_s3(s3_connection, local_path, dst_path, verbose=False):
|
||||
kw['cb'] = callback
|
||||
|
||||
k.set_contents_from_filename(logfile, **kw)
|
||||
|
||||
|
||||
def get_connection():
|
||||
return boto.connect_s3(g.S3KEY_ID or None, g.S3SECRET_KEY or None)
|
||||
|
||||
|
||||
def get_key(bucket_name, key, connection=None):
|
||||
connection = connection or get_connection()
|
||||
bucket = connection.get_bucket(bucket_name)
|
||||
|
||||
return bucket.get_key(key)
|
||||
|
||||
def get_keys(bucket_name, meta=False, connection=None, **kwargs):
|
||||
connection = connection or get_connection()
|
||||
bucket = connection.get_bucket(bucket_name)
|
||||
keys = bucket.get_all_keys(**kwargs)
|
||||
|
||||
if not meta:
|
||||
return keys
|
||||
|
||||
return [bucket.get_key(key.name)
|
||||
for key in keys]
|
||||
|
||||
|
||||
def delete_keys(bucket_name, prefix, connection=None):
|
||||
connection = connection or get_connection()
|
||||
|
||||
keys = get_keys(bucket_name, prefix=prefix, connection=connection)
|
||||
return connection.get_bucket(bucket_name).delete_keys(keys)
|
||||
|
||||
|
||||
def _get_upload_policy(
|
||||
bucket, key, acl, ttl=60,
|
||||
success_action_redirect=None,
|
||||
success_action_status="201",
|
||||
content_type=None,
|
||||
max_content_length=((1024**2) * 3),
|
||||
storage_class="STANDARD",
|
||||
meta=None,
|
||||
connection=None,
|
||||
):
|
||||
|
||||
connection = connection or get_connection()
|
||||
meta = meta or {}
|
||||
|
||||
expiration = time.gmtime(int(time.time() + ttl))
|
||||
conditions = []
|
||||
|
||||
conditions.append({"bucket": bucket})
|
||||
|
||||
if key.endswith("${filename}"):
|
||||
conditions.append(["starts-with", "$key", key[:-len("${filename}")]])
|
||||
else:
|
||||
conditions.append({"key": key})
|
||||
|
||||
conditions.append({"acl": acl})
|
||||
conditions.append({"x-amz-storage-class": storage_class})
|
||||
|
||||
if success_action_redirect:
|
||||
conditions.append([
|
||||
"starts-with",
|
||||
"$success_action_redirect",
|
||||
success_action_redirect,
|
||||
])
|
||||
else:
|
||||
conditions.append({
|
||||
"success_action_status": success_action_status,
|
||||
})
|
||||
|
||||
conditions.append([
|
||||
"content-length-range", 0, max_content_length])
|
||||
|
||||
for key, value in meta.iteritems():
|
||||
conditions.append({key: value})
|
||||
|
||||
if content_type:
|
||||
conditions.append({"content-type": content_type})
|
||||
|
||||
return base64.b64encode(json.dumps({
|
||||
"expiration": time.strftime(boto.utils.ISO8601, expiration),
|
||||
"conditions": conditions,
|
||||
}))
|
||||
|
||||
|
||||
def _get_upload_signature(
|
||||
policy,
|
||||
connection=None,
|
||||
):
|
||||
|
||||
connection = connection or get_connection()
|
||||
|
||||
key = connection.provider.secret_key.encode("utf-8")
|
||||
hashed = hmac.new(key, policy, hashlib.sha1)
|
||||
return base64.encodestring(
|
||||
hashed.digest()).decode("utf-8").strip()
|
||||
|
||||
|
||||
def get_post_args(
|
||||
bucket, key,
|
||||
acl="public-read",
|
||||
success_action_redirect=None,
|
||||
success_action_status="201",
|
||||
content_type=None,
|
||||
storage_class="STANDARD",
|
||||
meta=None,
|
||||
connection=None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
meta = meta or []
|
||||
connection = connection or get_connection()
|
||||
policy = _get_upload_policy(
|
||||
bucket=bucket,
|
||||
key=key,
|
||||
acl=acl,
|
||||
success_action_redirect=success_action_redirect,
|
||||
success_action_status=success_action_status,
|
||||
content_type=content_type,
|
||||
storage_class=storage_class,
|
||||
meta=meta,
|
||||
connection=connection,
|
||||
)
|
||||
signature = _get_upload_signature(
|
||||
policy, connection=connection)
|
||||
|
||||
fields = []
|
||||
|
||||
fields.append({
|
||||
"name": "AWSAccessKeyId",
|
||||
"value": connection.provider.access_key,
|
||||
})
|
||||
|
||||
fields.append({
|
||||
"name": "acl",
|
||||
"value": acl,
|
||||
})
|
||||
|
||||
fields.append({
|
||||
"name": "key",
|
||||
"value": key,
|
||||
})
|
||||
|
||||
if success_action_redirect:
|
||||
fields.append({
|
||||
"name": "success_action_redirect",
|
||||
"value": success_action_redirect,
|
||||
})
|
||||
else:
|
||||
fields.append({
|
||||
"name": "success_action_status",
|
||||
"value": success_action_status,
|
||||
})
|
||||
|
||||
fields.append({
|
||||
"name": "content-type",
|
||||
"value": content_type,
|
||||
})
|
||||
|
||||
fields.append({
|
||||
"name": "x-amz-storage-class",
|
||||
"value": storage_class,
|
||||
})
|
||||
|
||||
for key, value in meta.iteritems():
|
||||
fields.append({
|
||||
"name": key,
|
||||
"value": value,
|
||||
})
|
||||
|
||||
fields.append({
|
||||
"name": "policy",
|
||||
"value": policy,
|
||||
})
|
||||
|
||||
fields.append({
|
||||
"name": "signature",
|
||||
"value": signature,
|
||||
})
|
||||
|
||||
return {
|
||||
"action": "//%s.%s" % (bucket, g.s3_media_domain),
|
||||
"fields": fields,
|
||||
}
|
||||
|
||||
|
||||
BIN
r2/r2/public/static/600x314-placeholder.png
Normal file
BIN
r2/r2/public/static/600x314-placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
r2/r2/public/static/70x70-placeholder.png
Normal file
BIN
r2/r2/public/static/70x70-placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -13,3 +13,5 @@
|
||||
@import "toggles.less";
|
||||
@import "read-next.less";
|
||||
@import "infobar.less";
|
||||
@import "progress.less";
|
||||
@import "image-upload.less";
|
||||
|
||||
38
r2/r2/public/static/css/components/image-upload.less
Normal file
38
r2/r2/public/static/css/components/image-upload.less
Normal file
@@ -0,0 +1,38 @@
|
||||
.c-image-upload-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: block;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.c-image-upload-preview-container {
|
||||
border-width: 2px;
|
||||
border-style: dashed;
|
||||
border-color: lightgray;
|
||||
display: inline-block;
|
||||
margin: 5px 0;
|
||||
overflow: hidden;
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c-image-upload-preview {
|
||||
display: block;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.c-image-upload-btn {
|
||||
display: block;
|
||||
padding: 2px 8px !important;
|
||||
font-size: 10px !important;;
|
||||
}
|
||||
19
r2/r2/public/static/css/components/progress.less
Normal file
19
r2/r2/public/static/css/components/progress.less
Normal file
@@ -0,0 +1,19 @@
|
||||
.c-progress {
|
||||
display: none;
|
||||
height: 2px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
.box-shadow(~"inset 0 1px 2px rgba(0,0,0,.1)");
|
||||
position: relative;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.c-progress-bar {
|
||||
float: left;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
line-height: 2px;
|
||||
background-color: #337ab7;
|
||||
.box-shadow(~"inset 0 -1px 0 rgba(0,0,0,.15)");
|
||||
.transition-shorthand(width .6s ease);
|
||||
}
|
||||
288
r2/r2/public/static/js/image-upload.js
Normal file
288
r2/r2/public/static/js/image-upload.js
Normal file
@@ -0,0 +1,288 @@
|
||||
!(function(global, $, r, undefined) {
|
||||
'use strict';
|
||||
|
||||
var CALLBACK_PREFIX = '__image_upload__';
|
||||
|
||||
var DEFAULTS = {
|
||||
max: 1024 * 500,
|
||||
errors: {
|
||||
unknown: r._('something went wrong.'),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var ImageUpload = function(element, options) {
|
||||
this.initialize(element, options);
|
||||
};
|
||||
|
||||
_.extend(ImageUpload.prototype, {
|
||||
_callbacks: {},
|
||||
|
||||
initialize: function(element, options) {
|
||||
this.el = element;
|
||||
this.$el = $(element);
|
||||
this.$file = this.$el.find('[type="file"]');
|
||||
this.file = this.$file.get(0);
|
||||
this.options = _.defaults({}, this.$el.data(), options, DEFAULTS);
|
||||
this._bindEvents();
|
||||
this.ajax = !!global.FormData;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_bindEvents: function() {
|
||||
this.$file.on('change', this._handleChange.bind(this));
|
||||
this.$el.find('.c-image-upload-btn')
|
||||
.on('click',this._triggerDialog.bind(this));
|
||||
},
|
||||
|
||||
_triggerDialog: function() {
|
||||
this.$file.click();
|
||||
},
|
||||
|
||||
_handleChange: function() {
|
||||
if (this.file.value) {
|
||||
var files = this.file.files;
|
||||
|
||||
if (files && files.length && files[0].size > this.options.maxSize) {
|
||||
this.$el.trigger('failed.imageUpload', [{
|
||||
message: r._('too big. keep it under ' + r.utils.formatFileSize(this.options.maxSize)),
|
||||
}]);
|
||||
|
||||
this._reset();
|
||||
} else {
|
||||
$.ajax({
|
||||
url: this.options.url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: _.extend({}, this.options.params, {
|
||||
filepath: files[0].name,
|
||||
uh: reddit.modhash,
|
||||
ajax: this.ajax,
|
||||
raw_json: '1',
|
||||
}),
|
||||
})
|
||||
.fail(function(xhr) {
|
||||
var resp = xhr.responseJSON;
|
||||
|
||||
this.$el.trigger('failed.imageUpload', [{
|
||||
message: resp.message || this.options.errors.unknown,
|
||||
}]);
|
||||
}.bind(this))
|
||||
.done(this._submit.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
_updateProgress: function(percentage) {
|
||||
this._showProgress();
|
||||
|
||||
if (percentage) {
|
||||
this._percentage = percentage;
|
||||
this.$el.find('.c-progress-bar').css({ width: (percentage + '%') });
|
||||
}
|
||||
},
|
||||
|
||||
_showProgress: function() {
|
||||
this.$el.find('.c-progress').show();
|
||||
},
|
||||
|
||||
_hideProgress: function() {
|
||||
this.$el.find('.c-progress').hide();
|
||||
this.$el.find('.c-progress-bar').css({ width: 0 });
|
||||
},
|
||||
|
||||
_submit: function(overrides) {
|
||||
overrides = overrides || {};
|
||||
|
||||
this.$el.attr('action', overrides.action);
|
||||
|
||||
overrides.fields.forEach(function(field) {
|
||||
var name = field.name;
|
||||
var value = field.value;
|
||||
var unset = value === '' || value === null
|
||||
var $input = this.$el.find('[name="' + name + '"]');
|
||||
|
||||
if (!$input.length) {
|
||||
// Skip null values
|
||||
if (unset) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('<input type="hidden">')
|
||||
.attr('name', name)
|
||||
.val(value)
|
||||
.insertBefore(this.$file);
|
||||
} else {
|
||||
if (unset) {
|
||||
$input.remove();
|
||||
} else {
|
||||
$input.val(value);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
if (!this.ajax) {
|
||||
var $redirect = this.$el.find('[name="success_action_redirect"]');
|
||||
var redirect = $redirect.val();
|
||||
var callback = _.uniqueId(CALLBACK_PREFIX);
|
||||
|
||||
redirect = r.utils.replaceUrlParams(redirect, {callback: callback});
|
||||
|
||||
$redirect.val(redirect);
|
||||
|
||||
global.__s3_callbacks__[callback] = this._callbacks[callback] = this._iframeComplete.bind(this);
|
||||
this._fakeProgressTo(10, 80);
|
||||
this.$el.submit();
|
||||
} else {
|
||||
var fields = {};
|
||||
var data = new FormData();
|
||||
var fileInput;
|
||||
|
||||
this.$el.find('input').each(function() {
|
||||
var el = this;
|
||||
var $el = $(el);
|
||||
var type = $el.attr('type');
|
||||
|
||||
if (type !== 'file') {
|
||||
data.append($el.attr('name'), $el.val());
|
||||
}
|
||||
});
|
||||
|
||||
data.append(this.file.name, this.file.files[0]);
|
||||
|
||||
this._showProgress();
|
||||
|
||||
$.ajax({
|
||||
url: this.$el.attr('action'),
|
||||
type: this.$el.attr('method'),
|
||||
contentType: false,
|
||||
processData: false,
|
||||
data: data,
|
||||
dataType: 'xml',
|
||||
success: this._ajaxSuccess.bind(this),
|
||||
error: this._ajaxError.bind(this),
|
||||
progress: this._ajaxProgress.bind(this),
|
||||
complete: this._reset.bind(this),
|
||||
xhr: function() {
|
||||
var xhr = $.ajaxSettings.xhr();
|
||||
|
||||
if (xhr instanceof global.XMLHttpRequest) {
|
||||
xhr.addEventListener('progress', this.progress, false);
|
||||
}
|
||||
|
||||
if (xhr.upload) {
|
||||
xhr.upload.addEventListener('progress', this.progress, false);
|
||||
}
|
||||
|
||||
return xhr;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.$el.trigger('uploading.imageUpload');
|
||||
},
|
||||
|
||||
_fakeProgressTo: function(start, end) {
|
||||
var _fakeProgressInterval = this._fakeProgress = setInterval(function() {
|
||||
if (this._percentage > end) {
|
||||
clearInterval(_fakeProgressInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateProgress((this._percentage || start) + 1);
|
||||
}.bind(this), 200);
|
||||
},
|
||||
|
||||
_ajaxProgress: function(e) {
|
||||
var percentage = Math.round((e.loaded / e.total) * 100);
|
||||
|
||||
this._updateProgress(percentage);
|
||||
|
||||
this.$el.trigger('progress.imageUpload', [{
|
||||
complete: e.loaded,
|
||||
total: e.total,
|
||||
percentage: percentage,
|
||||
}]);
|
||||
},
|
||||
|
||||
_updatePreview: function (url, callback) {
|
||||
callback = callback || $.noop;
|
||||
|
||||
var $preview = this.$el.find('.c-image-upload-preview');
|
||||
var src = r.utils.replaceUrlParams(url, {
|
||||
cb: (+new Date()),
|
||||
});
|
||||
|
||||
$preview.one('load', callback.bind(this));
|
||||
$preview.attr('src', src);
|
||||
},
|
||||
|
||||
_ajaxSuccess: function(xml) {
|
||||
var $xml = $(xml);
|
||||
var url = $xml.find('Location').text();
|
||||
|
||||
this._updatePreview(url, this._hideProgress);
|
||||
this._updateProgress(100);
|
||||
this.$el.trigger('success.imageUpload', [{
|
||||
url: url,
|
||||
}]);
|
||||
},
|
||||
|
||||
_ajaxError: function(xhr) {
|
||||
var $xml = $(xhr.responseXML);
|
||||
var message = this.options.errors.unknown;
|
||||
|
||||
if ($xml && $xml.length) {
|
||||
message = $xml.find('Message').text();
|
||||
}
|
||||
|
||||
this._hideProgress();
|
||||
this.$el.trigger('failed.imageUpload', [{
|
||||
message: message,
|
||||
}]);
|
||||
},
|
||||
|
||||
_iframeComplete: function(data) {
|
||||
this._updateProgress(100);
|
||||
this._updatePreview(data.url, this._hideProgress);
|
||||
},
|
||||
|
||||
_reset: function() {
|
||||
this.$file.resetInput();
|
||||
this.$el.trigger('reset.imageUpload');
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
function Plugin(option /* ,args... */) {
|
||||
var args = _.toArray(arguments).slice(1);
|
||||
|
||||
if (option && /^get/.test(option)) {
|
||||
var data = this.data('c.imageUpload');
|
||||
|
||||
return data && data[option].apply(data, args);
|
||||
}
|
||||
|
||||
return this.each(function() {
|
||||
var $el = $(this);
|
||||
var data = $el.data('c.imageUpload');
|
||||
var options = typeof option === 'object' && option;
|
||||
|
||||
if (!data) {
|
||||
data = new ImageUpload(this, options);
|
||||
$el.data('c.imageUpload', data);
|
||||
}
|
||||
|
||||
if (typeof option === 'string') {
|
||||
data[option].apply(data, args);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$.fn.imageUpload = Plugin;
|
||||
$.fn.imageUpload.Constructor = ImageUpload;
|
||||
|
||||
})(this, this.jQuery, this.r);
|
||||
@@ -322,6 +322,14 @@ $.fn.updateThing = function(update) {
|
||||
}
|
||||
}
|
||||
|
||||
$.fn.resetInput = function() {
|
||||
var $el = $(this);
|
||||
$el.wrap('<form>').closest('form').get(0).reset();
|
||||
$el.unwrap();
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
$.fn.show_unvotable_message = function() {
|
||||
$(this).thing().find(".entry:first .unvotable-message").css("display", "inline-block");
|
||||
};
|
||||
|
||||
@@ -500,6 +500,16 @@ var exports = r.sponsored = {
|
||||
this.inventory = {}
|
||||
this.campaignListColumns = $('.existing-campaigns thead th').length
|
||||
$("input[name='media_url_type']").on("change", this.mediaInputChange)
|
||||
|
||||
this.initUploads();
|
||||
},
|
||||
|
||||
initUploads: function() {
|
||||
$('.c-image-upload')
|
||||
.imageUpload()
|
||||
.on('failed.imageUpload', function(e, data) {
|
||||
alert(data.message);
|
||||
});
|
||||
},
|
||||
|
||||
setup: function(inventory_by_sr, priceDict, isEmpty, userIsSponsor) {
|
||||
|
||||
@@ -20,6 +20,15 @@ r.utils = {
|
||||
return a.href;
|
||||
},
|
||||
|
||||
// Returns human readable file sizes
|
||||
// http://stackoverflow.com/a/25613067/704286
|
||||
formatFileSize: function(size) {
|
||||
var suffixes = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'EiB', 'ZiB'];
|
||||
var order = size ? parseInt(Math.log2(size) / 10, 10) : 0;
|
||||
|
||||
return (size / (1 << (order * 10))).toFixed(3).replace(/\.?0+$/, '') + ' ' + suffixes[order];
|
||||
},
|
||||
|
||||
fullnameToId: function(fullname) {
|
||||
var parts = fullname.split('_');
|
||||
var id36 = parts && parts[1];
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<%!
|
||||
from r2.config import feature
|
||||
from r2.lib.media import thumbnail_url
|
||||
from r2.lib.filters import jssafe
|
||||
from r2.lib.filters import jssafe, scriptsafe_dumps
|
||||
from r2.lib.pages import UserText
|
||||
import simplejson
|
||||
from babel.numbers import format_currency
|
||||
@@ -170,68 +170,59 @@
|
||||
</%utils:line_field>
|
||||
</%def>
|
||||
|
||||
<%def name="image_field(link=None)">
|
||||
<%def name="image_field(link=None, images=None)">
|
||||
<%
|
||||
if link:
|
||||
current_image = getattr(link, 'thumbnail_url', '')
|
||||
current_mobile_ad_image = getattr(link, 'mobile_ad_url', '')
|
||||
link_name = link._fullname
|
||||
thumbnail_url = getattr(link, "thumbnail_url", None)
|
||||
mobile_url = getattr(link, "mobile_ad_url", None)
|
||||
path = "ads/%s" % link._id36
|
||||
else:
|
||||
current_image = ''
|
||||
current_mobile_ad_image =''
|
||||
link_name = ''
|
||||
thumbnail_url = images.get("thumbnail", None)
|
||||
mobile_url = images.get("mobile", None)
|
||||
path = "ads/%s" % c.user.name
|
||||
%>
|
||||
<%utils:line_field title="${_('thumbnail')}"
|
||||
css_class="rounded image-field ${'has-image' if current_image else ''}">
|
||||
<%utils:line_field
|
||||
title="${_('thumbnail')}"
|
||||
css_class="rounded image-field">
|
||||
<div class="infotext">
|
||||
${_('images will be resized if larger than 140x140 pixels (displayed at 70x70)')}
|
||||
</div>
|
||||
<div class="delete-field">
|
||||
%if link_name:
|
||||
<%utils:image_upload post_target="/api/link_thumb"
|
||||
form_id="image-upload"
|
||||
current_image="${current_image}"
|
||||
label="${_('upload header image:')}"
|
||||
ask_type="True">
|
||||
<input type="hidden" name="link_name" value="${link_name}" />
|
||||
</%utils:image_upload>
|
||||
%else:
|
||||
${utils.image_upload_inline()}
|
||||
%endif
|
||||
|
||||
<div class="clearleft"></div>
|
||||
</div>
|
||||
${utils.s3_image_upload(
|
||||
id="thumbnail",
|
||||
width="70",
|
||||
height="70",
|
||||
src=thumbnail_url,
|
||||
data=dict(
|
||||
max=(1024**2) * 3,
|
||||
url="/api/ad_s3_params.json",
|
||||
params=simplejson.dumps(dict(
|
||||
kind="thumbnail",
|
||||
link=(link and link._id36),
|
||||
)),
|
||||
),
|
||||
)}
|
||||
</%utils:line_field>
|
||||
|
||||
%if c.user_is_sponsor and link_name:
|
||||
<%utils:line_field title="${_('mobile ad image')}"
|
||||
css_class="rounded image-field">
|
||||
<div class="infotext">
|
||||
${_('upload image for use on mobile web. should be exactly 1200x628 pixels (displayed at 600x314)')}
|
||||
</div>
|
||||
<%utils:image_upload post_target="/api/link_mobile_ad_image"
|
||||
form_id="mobile-ad-image-upload"
|
||||
current_image="${current_mobile_ad_image}"
|
||||
label="${_('upload mobile ad image:')}"
|
||||
ask_type="True">
|
||||
<input type="hidden" name="link_name" value="${link_name}" />
|
||||
</%utils:image_upload>
|
||||
</%utils:line_field>
|
||||
%endif
|
||||
|
||||
## overwrite the completed image function
|
||||
<script type="text/javascript">
|
||||
completedUploadImage = (function(cu) {
|
||||
return function(status, img_src, name, errors, form_id) {
|
||||
cu(status, "", "", errors, form_id);
|
||||
if (form_id === 'image-upload') {
|
||||
$.things('${jssafe(link_name)}')
|
||||
.find(".thumbnail img")
|
||||
.attr("src", img_src + "?v=" + Math.random());
|
||||
}
|
||||
}
|
||||
})(completedUploadImage);
|
||||
</script>
|
||||
<%utils:line_field title="${_('mobile ad image')}"
|
||||
css_class="rounded image-field">
|
||||
<div class="infotext">
|
||||
${_('upload image for use on mobile web. should be exactly 1200x628 pixels (displayed at 600x314)')}
|
||||
</div>
|
||||
${utils.s3_image_upload(
|
||||
id="mobile",
|
||||
width="600",
|
||||
height="314",
|
||||
src=mobile_url,
|
||||
data=dict(
|
||||
max=(1024**2) * 3,
|
||||
url="/api/ad_s3_params.json",
|
||||
params=simplejson.dumps(dict(
|
||||
kind="mobile",
|
||||
link=(link and link._id36),
|
||||
)),
|
||||
),
|
||||
)}
|
||||
</%utils:line_field>
|
||||
</%def>
|
||||
|
||||
<%def name="commenting_field(link)">
|
||||
|
||||
@@ -184,12 +184,10 @@ ${pr.javascript_setup()}
|
||||
</footer>
|
||||
</div>
|
||||
<div class="uncollapsed-display" style="display:none">
|
||||
%if editable:
|
||||
<div class="editor-group">
|
||||
${pr.image_field(thing.link)}
|
||||
</div>
|
||||
%endif
|
||||
<div class="editor-group">
|
||||
%if editable:
|
||||
${pr.image_field(thing.link)}
|
||||
%endif
|
||||
<% is_link = not thing.link.is_self %>
|
||||
${pr.title_field(thing.link, editable=editable)}
|
||||
${pr.content_field(thing.link, editable=editable, enable_override=c.user_is_sponsor)}
|
||||
|
||||
@@ -20,10 +20,15 @@
|
||||
## reddit Inc. All Rights Reserved.
|
||||
###############################################################################
|
||||
|
||||
<%!
|
||||
from r2.lib import js
|
||||
%>
|
||||
|
||||
<%namespace file="promotelinkbase.html" import="title_field, content_field, managed_field, image_field" />
|
||||
<%namespace file="utils.html" import="error_field" />
|
||||
<%namespace name="utils" file="utils.html"/>
|
||||
|
||||
${unsafe(js.use('sponsored'))}
|
||||
|
||||
<div class="create-promotion sponsored-page">
|
||||
<div class="dashboard">
|
||||
@@ -31,39 +36,33 @@
|
||||
<h2>new promotion</h2>
|
||||
</header>
|
||||
<div class="dashboard-content">
|
||||
<form method="post" action="/api/create_promo"
|
||||
class="pretty-form promotelink-editor editor"
|
||||
id="promo-form"
|
||||
target="upload-iframe"
|
||||
enctype="multipart/form-data">
|
||||
<div class="pretty-form promotelink-editor editor" id="promo-form">
|
||||
## need to set the modhash because we're not using a helper method to post the form
|
||||
<input type="hidden" name="uh" value="${c.modhash}">
|
||||
<input type="hidden" name="id" value="#promo-form">
|
||||
|
||||
<div class="editor-group">
|
||||
${image_field()}
|
||||
</div>
|
||||
<div class="editor-group">
|
||||
%if c.user_is_sponsor:
|
||||
${username_field()}
|
||||
${managed_field(None)}
|
||||
%endif
|
||||
${title_field(None, editable=True)}
|
||||
${content_field(None, editable=True, enable_override=c.user_is_sponsor,
|
||||
tracker_access=c.user_can_track_ads)}
|
||||
<footer class="buttons">
|
||||
<div class="rules">
|
||||
By clicking "next" you agree to the <a href="https://www.reddit.com/wiki/selfserve#wiki_online_self_serve_advertising_rules" target="_blank">Self Serve Advertising Rules.</a>
|
||||
</div>
|
||||
|
||||
${error_field("RATELIMIT", "ratelimit")}
|
||||
 
|
||||
<span class="status error"></span>
|
||||
${error_field("RATELIMIT", "ratelimit")}
|
||||
<button name="create" class="btn primary-button" type="submit">
|
||||
${_("next")}
|
||||
</button>
|
||||
</footer>
|
||||
${image_field(images=thing.images)}
|
||||
%if c.user_is_sponsor:
|
||||
${username_field()}
|
||||
${managed_field(None)}
|
||||
%endif
|
||||
${title_field(None, editable=True)}
|
||||
${content_field(None, editable=True, enable_override=c.user_is_sponsor,
|
||||
tracker_access=c.user_can_track_ads)}
|
||||
<footer class="buttons">
|
||||
<div class="rules">
|
||||
By clicking "next" you agree to the <a href="http://www.reddit.com/wiki/selfserve#wiki_online_self_serve_advertising_rules" target="_blank">Self Serve Advertising Rules.</a>
|
||||
</div>
|
||||
${error_field("RATELIMIT", "ratelimit")}
|
||||
 
|
||||
<span class="status error"></span>
|
||||
${error_field("RATELIMIT", "ratelimit")}
|
||||
<button
|
||||
name="create" class="btn primary-button" type="button"
|
||||
onclick="return post_pseudo_form('#promo-form', 'create_promo')">
|
||||
${_("next")}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
<iframe src="about:blank" width="600" height="200"
|
||||
@@ -85,3 +84,7 @@
|
||||
</div>
|
||||
</%utils:line_field>
|
||||
</%def>
|
||||
|
||||
<script type="text/javascript">
|
||||
r.sponsored.initUploads();
|
||||
</script>
|
||||
|
||||
0
r2/r2/templates/uploadedadsimage.html
Normal file
0
r2/r2/templates/uploadedadsimage.html
Normal file
@@ -21,13 +21,20 @@
|
||||
###############################################################################
|
||||
|
||||
<%!
|
||||
import json
|
||||
from r2.models import FakeSubreddit
|
||||
from r2.lib.filters import spaceCompress, unsafe, safemarkdown, jssafe
|
||||
from r2.lib.template_helpers import add_sr, js_config, static, html_datetime, simplified_timesince, make_url_protocol_relative
|
||||
from r2.lib.utils import cols, long_datetime
|
||||
from r2.lib import tracking
|
||||
from datetime import datetime
|
||||
import json
|
||||
from r2.models import FakeSubreddit
|
||||
from r2.lib.filters import spaceCompress, unsafe, safemarkdown, jssafe
|
||||
from r2.lib.template_helpers import (
|
||||
add_sr,
|
||||
html_datetime,
|
||||
js_config,
|
||||
make_url_protocol_relative,
|
||||
simplified_timesince,
|
||||
static,
|
||||
)
|
||||
from r2.lib.utils import cols, long_datetime
|
||||
from r2.lib import tracking
|
||||
from datetime import datetime
|
||||
%>
|
||||
<%def name="tags(**kw)">
|
||||
%for k, v in kw.iteritems():
|
||||
@@ -291,6 +298,39 @@ ${unsafe(txt)}
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="s3_image_upload(id, width, height, src=None, data=None)">
|
||||
<form
|
||||
id="${id}"
|
||||
class="c-image-upload"
|
||||
method="POST"
|
||||
enctype="multipart/form-data"
|
||||
target="${id}-frame"
|
||||
${tags(**dict(data=data))}
|
||||
>
|
||||
<div class="c-image-upload-preview-container">
|
||||
<img
|
||||
alt="${id} preview"
|
||||
class="c-image-upload-preview"
|
||||
style="width: ${width}px; max-height: ${height}px"
|
||||
%if src:
|
||||
src="${make_url_protocol_relative(src)}"
|
||||
%else:
|
||||
src="${static(width + 'x' + height + '-placeholder.png')}"
|
||||
%endif
|
||||
>
|
||||
<input type="file" name="file" id="${id}-input" class="c-image-upload-input" title="${_("click to upload")}">
|
||||
<div class="c-progress">
|
||||
<div class="c-progress-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="c-image-upload-btn">${_("upload")}</button>
|
||||
</form>
|
||||
<iframe id="${id}-frame" name="${id}-frame" src="about:blank" style="display:none;"></iframe>
|
||||
%if caller:
|
||||
${caller.body()}
|
||||
%endif
|
||||
</%def>
|
||||
|
||||
<%def name="image_upload(post_target, current_image = None, onsubmit = '',
|
||||
onchange = '', label = '', form_id = 'image-upload',
|
||||
ask_type = False, hidden_data=None)">
|
||||
|
||||
Reference in New Issue
Block a user