From bf9f43ccacc4ee7933629c621f97d6ff0ddd11b8 Mon Sep 17 00:00:00 2001 From: KeyserSosa Date: Tue, 1 Dec 2009 13:42:06 -0800 Subject: [PATCH] Messaging/commenting === - add confidence sorting to comments * common values are precomputed for speedier response * best is made the default sort on comment pages - messages will now be delivered once one is moderator/contributor/banned - UI updates to messaging page, including added show parent functionality to messages - Remove the rate-limit on comments on your own self-posts - Give users some leeway in editing their comments: don't show an edit star if the edit is within the first few minutes of a comment's lifetime - Office Assistant will help users when they write to admins Backend === - Replace the postgres-based query_queue with an AMQP based one * Set up amqp queues for async tasks such as search updates and the scrapers * service monitor updates, adding queue-tracking support - Allow find_recent_broken_things to specify both from_time and to_time - add a ini file parameter to disallow db writes (to create read-only reddit instances for crawlers) New features === - self-serve advertisement: * complete overhaul of sponsored link code * functions for talking with authorize.net * added pay domain and https support * added ability to share traffic from sponsored links * auto-reject promotions that are too old and unpaid for - awards - allow widget to have its links to have a target (in case it is iframed) - automatic_reddits: * Don't show automatic_reddits in the horizontal topbar - Listing numbers are always in order with no gaps - add support for sprites for common (r2.lib.contrib.nymph) Admin === - added a takedown page for dealing with DMCA requests properly * status code 404 on takedown pages * JSON returns same string as in the explanation text * nofollow on markdown in explanation * title and image optional - Added /c/(comment_id) for admins - updates to JS to rate-limit voting, commenting, and anything else that could be just as easily done by a script-kiddie to cheat. - make ad frame dynamic and add tracking pixel - add the ability to add a sponsored banner to the rightbox of a reddit - add the ability to show custom css on cnamed and/or non-cnamed versions of a reddit - allow us to ignore reports from report-spammers. Bugfixes === - Fix sorting of duplicate links (patch by Chromakode) - fix traffic bug on main traffic page when it is the first of the month. - toolbar redirects to comments page on self posts rather than generating the frame - half-assed unicode handling in menus giving us bugs again. Switched to the whole-ass approach - added Thing._byID36 - Support /help/foo/bar --- r2/Makefile | 18 +- r2/draw_load.py | 87 + r2/example.ini | 37 +- r2/r2/config/middleware.py | 6 +- r2/r2/config/routing.py | 35 +- r2/r2/config/templates.py | 2 + r2/r2/controllers/__init__.py | 2 + r2/r2/controllers/api.py | 449 +- r2/r2/controllers/awards.py | 54 + r2/r2/controllers/embed.py | 10 +- r2/r2/controllers/error.py | 7 +- r2/r2/controllers/errors.py | 12 +- r2/r2/controllers/feedback.py | 16 +- r2/r2/controllers/front.py | 107 +- r2/r2/controllers/health.py | 52 + r2/r2/controllers/listingcontroller.py | 35 +- r2/r2/controllers/post.py | 13 +- r2/r2/controllers/promotecontroller.py | 418 +- r2/r2/controllers/reddit_base.py | 31 +- r2/r2/controllers/toolbar.py | 4 +- r2/r2/controllers/validator/validator.py | 386 +- r2/r2/i18n/r2.pot | 1500 +++--- r2/r2/lib/amqp.py | 211 + r2/r2/lib/app_globals.py | 42 +- r2/r2/lib/authorize/__init__.py | 22 + r2/r2/lib/authorize/api.py | 589 +++ r2/r2/lib/authorize/interaction.py | 176 + r2/r2/lib/base.py | 2 + r2/r2/lib/contrib/nymph.py | 93 + r2/r2/lib/cssfilter.py | 13 +- r2/r2/lib/db/queries.py | 63 +- r2/r2/lib/db/query_queue.py | 144 +- r2/r2/lib/db/sorts.py | 27 +- r2/r2/lib/db/tdb_sql.py | 14 +- r2/r2/lib/db/thing.py | 24 +- r2/r2/lib/emailer.py | 200 +- r2/r2/lib/filters.py | 1 - r2/r2/lib/jsonresponse.py | 8 +- r2/r2/lib/jsontemplates.py | 23 +- r2/r2/lib/media.py | 89 +- r2/r2/lib/menus.py | 47 +- r2/r2/lib/migrate.py | 102 + r2/r2/lib/normalized_hot.py | 2 +- r2/r2/lib/organic.py | 39 +- r2/r2/lib/pages/admin_pages.py | 5 +- r2/r2/lib/pages/graph.py | 8 +- r2/r2/lib/pages/pages.py | 745 ++- r2/r2/lib/pages/things.py | 28 +- r2/r2/lib/promote.py | 509 +- r2/r2/lib/services.py | 69 +- r2/r2/lib/solrsearch.py | 93 +- r2/r2/lib/spreadshirt.py | 2 +- r2/r2/lib/strings.py | 29 +- r2/r2/lib/template_helpers.py | 84 +- r2/r2/lib/tracking.py | 14 +- r2/r2/lib/traffic.py | 10 +- r2/r2/lib/translation.py | 10 +- r2/r2/lib/user_stats.py | 72 - r2/r2/lib/utils/utils.py | 105 +- r2/r2/lib/workqueue.py | 17 +- r2/r2/lib/wrapped.py | 5 +- r2/r2/models/__init__.py | 2 + r2/r2/models/account.py | 61 +- r2/r2/models/admintools.py | 53 +- r2/r2/models/award.py | 106 + r2/r2/models/bidding.py | 442 ++ r2/r2/models/builder.py | 63 +- r2/r2/models/link.py | 141 +- r2/r2/models/mail_queue.py | 67 +- r2/r2/models/populatedb.py | 4 - r2/r2/models/printable.py | 2 +- r2/r2/models/subreddit.py | 55 +- r2/r2/models/thing_changes.py | 85 +- r2/r2/models/vote.py | 8 +- r2/r2/public/static/alien-clippy.png | Bin 0 -> 2537 bytes r2/r2/public/static/award.png | Bin 0 -> 218 bytes r2/r2/public/static/bg-button-add.png | Bin 0 -> 137 bytes r2/r2/public/static/bg-button-remove.png | Bin 0 -> 143 bytes r2/r2/public/static/cclogo.png | Bin 0 -> 5378 bytes r2/r2/public/static/clippy-bullet.png | Bin 0 -> 349 bytes r2/r2/public/static/css/reddit-ie6-hax.css | 20 + r2/r2/public/static/css/reddit-ie7-hax.css | 8 + r2/r2/public/static/css/reddit.css | 1320 +++-- r2/r2/public/static/css/spreadshirt.css | 16 +- r2/r2/public/static/dragonage/bgfinal.jpg | Bin 0 -> 218450 bytes r2/r2/public/static/dragonage/topb.jpg | Bin 0 -> 56432 bytes r2/r2/public/static/gagged-alien.png | Bin 0 -> 4454 bytes r2/r2/public/static/gradient-button-hover.png | Bin 0 -> 177 bytes r2/r2/public/static/gradient-button.png | Bin 0 -> 149 bytes r2/r2/public/static/gradient-nub-hover.png | Bin 0 -> 773 bytes r2/r2/public/static/gradient-nub.png | Bin 0 -> 732 bytes r2/r2/public/static/js/jquery-1.3.1.js | 4241 +++++++++++++++++ r2/r2/public/static/js/jquery-1.3.1.min.js | 19 + r2/r2/public/static/js/jquery.js | 2 +- r2/r2/public/static/js/jquery.reddit.js | 36 +- r2/r2/public/static/js/reddit.js | 88 +- r2/r2/public/static/js/sponsored.js | 64 + r2/r2/public/static/js/ui.core.js | 519 ++ r2/r2/public/static/js/ui.datepicker.js | 1630 +++++++ r2/r2/public/static/reddit_ban.png | Bin 0 -> 767 bytes r2/r2/public/static/reddit_edit.png | Bin 0 -> 716 bytes r2/r2/public/static/reddit_reported.png | Bin 0 -> 665 bytes r2/r2/public/static/reddit_spam.png | Bin 0 -> 701 bytes r2/r2/public/static/reddit_traffic.png | Bin 0 -> 526 bytes .../static/redditaddict/appsbar/barbg.png | Bin 0 -> 161 bytes .../static/redditaddict/appsbar/buttonbg.png | Bin 0 -> 247 bytes .../static/redditaddict/appsbar/nub.png | Bin 0 -> 208 bytes .../redditaddict/badge/AIRInstallBadge.swf | Bin 0 -> 27864 bytes .../static/redditaddict/badge/badgeimage.jpg | Bin 0 -> 13353 bytes .../redditaddict/badge/expressinstall.swf | Bin 0 -> 4823 bytes .../static/redditaddict/badge/swfobject.js | 8 + .../static/redditaddict/images/arrows.png | Bin 0 -> 1200 bytes .../public/static/redditaddict/images/bg.png | Bin 0 -> 214 bytes .../static/redditaddict/images/black50.png | Bin 0 -> 118 bytes .../static/redditaddict/images/c-caps.png | Bin 0 -> 396 bytes .../static/redditaddict/images/c-tile.png | Bin 0 -> 124 bytes .../static/redditaddict/images/close.png | Bin 0 -> 1912 bytes .../redditaddict/images/reddit-head.png | Bin 0 -> 1486 bytes .../static/redditaddict/images/ss-graph.jpg | Bin 0 -> 21663 bytes .../static/redditaddict/images/ss-main.jpg | Bin 0 -> 32702 bytes .../static/redditaddict/images/star-blue.png | Bin 0 -> 939 bytes .../redditaddict/images/star-orangered.png | Bin 0 -> 908 bytes .../static/redditaddict/images/star-white.png | Bin 0 -> 855 bytes .../static/redditaddict/images/support.png | Bin 0 -> 4166 bytes .../static/redditaddict/images/thumbsup3.png | Bin 0 -> 2004 bytes .../static/redditaddict/images/w-bot.png | Bin 0 -> 1613 bytes .../static/redditaddict/images/w-mid.png | Bin 0 -> 193 bytes .../static/redditaddict/images/w-top.png | Bin 0 -> 18121 bytes r2/r2/public/static/redditaddict/index.html | 533 +++ .../public/static/socialite/appsbar/barbg.png | Bin 0 -> 161 bytes .../static/socialite/appsbar/buttonbg.png | Bin 0 -> 247 bytes r2/r2/public/static/socialite/appsbar/nub.png | Bin 0 -> 208 bytes r2/r2/public/static/socialite/index.html | 442 +- .../public/static/socialite/socialitelogo.png | Bin 15477 -> 19793 bytes r2/r2/templates/adminawardgive.html | 84 + r2/r2/templates/adminawards.html | 96 + ...{userstats.html => adminawardwinners.html} | 69 +- r2/r2/templates/admintranslations.html | 2 +- r2/r2/templates/ads.html | 34 +- r2/r2/templates/appservicemonitor.html | 35 +- r2/r2/templates/base.htmllite | 18 +- r2/r2/templates/clickgadget.html | 6 +- r2/r2/templates/comment.html | 7 +- r2/r2/templates/comment.htmllite | 8 +- r2/r2/templates/comment.mobile | 8 +- r2/r2/templates/createsubreddit.html | 432 +- r2/r2/templates/dart_ad.html | 51 + r2/r2/templates/link.html | 3 +- r2/r2/templates/link.htmllite | 23 +- r2/r2/templates/link.mobile | 6 +- r2/r2/templates/link.wired | 2 +- r2/r2/templates/linkinfobar.html | 49 +- r2/r2/templates/linkpromoteinfobar.html | 29 +- r2/r2/templates/message.html | 17 +- r2/r2/templates/messagecompose.html | 84 +- r2/r2/templates/morechildren.html | 3 +- r2/r2/templates/paymentform.html | 209 + r2/r2/templates/prefoptions.html | 5 + r2/r2/templates/prefupdate.html | 37 +- r2/r2/templates/printable.html | 53 +- r2/r2/templates/printablebuttons.html | 75 +- r2/r2/templates/profilebar.html | 140 +- r2/r2/templates/promo_email.email | 118 + r2/r2/templates/promote_graph.html | 276 ++ r2/r2/templates/promotedlink.html | 73 +- r2/r2/templates/promotedlinks.html | 83 - r2/r2/templates/promotedtraffic.html | 58 +- r2/r2/templates/promotelinkform.html | 465 +- r2/r2/templates/reddit.html | 28 +- r2/r2/templates/redditfooter.html | 2 +- r2/r2/templates/redditheader.html | 6 + r2/r2/templates/reddittraffic.html | 4 +- r2/r2/templates/selfserveblurb.html | 35 + r2/r2/templates/share.email | 5 +- r2/r2/templates/shirtpane.html | 11 + r2/r2/templates/sidebox.html | 8 +- r2/r2/templates/sidecontentbox.html | 35 + r2/r2/templates/sponsorshipbox.html | 41 + r2/r2/templates/subreddit.html | 8 +- r2/r2/templates/subredditinfobar.html | 129 +- r2/r2/templates/subredditstylesheet.html | 24 +- r2/r2/templates/subscriptionbox.html | 1 - r2/r2/templates/takedownpane.html | 35 + r2/r2/templates/translatedstring.html | 6 +- r2/r2/templates/trophycase.html | 85 + r2/r2/templates/uploadedimage.html | 3 +- r2/r2/templates/userawards.html | 62 + r2/r2/templates/userlist.html | 31 +- r2/r2/templates/usertableitem.html | 2 +- r2/r2/templates/usertext.html | 10 +- r2/r2/templates/utils.html | 69 +- r2/r2/templates/verifyemail.email | 31 + r2/r2/templates/wrappeduser.html | 66 + r2/setup.py | 11 +- r2/supervise_watcher.py | 8 +- 195 files changed, 17270 insertions(+), 3196 deletions(-) create mode 100644 r2/draw_load.py create mode 100644 r2/r2/controllers/awards.py create mode 100644 r2/r2/controllers/health.py create mode 100644 r2/r2/lib/amqp.py create mode 100644 r2/r2/lib/authorize/__init__.py create mode 100644 r2/r2/lib/authorize/api.py create mode 100644 r2/r2/lib/authorize/interaction.py create mode 100644 r2/r2/lib/contrib/nymph.py delete mode 100644 r2/r2/lib/user_stats.py create mode 100644 r2/r2/models/award.py create mode 100644 r2/r2/models/bidding.py create mode 100644 r2/r2/public/static/alien-clippy.png create mode 100644 r2/r2/public/static/award.png create mode 100644 r2/r2/public/static/bg-button-add.png create mode 100644 r2/r2/public/static/bg-button-remove.png create mode 100644 r2/r2/public/static/cclogo.png create mode 100644 r2/r2/public/static/clippy-bullet.png create mode 100644 r2/r2/public/static/dragonage/bgfinal.jpg create mode 100644 r2/r2/public/static/dragonage/topb.jpg create mode 100644 r2/r2/public/static/gagged-alien.png create mode 100644 r2/r2/public/static/gradient-button-hover.png create mode 100644 r2/r2/public/static/gradient-button.png create mode 100644 r2/r2/public/static/gradient-nub-hover.png create mode 100644 r2/r2/public/static/gradient-nub.png create mode 100644 r2/r2/public/static/js/jquery-1.3.1.js create mode 100644 r2/r2/public/static/js/jquery-1.3.1.min.js create mode 100644 r2/r2/public/static/js/sponsored.js create mode 100644 r2/r2/public/static/js/ui.core.js create mode 100644 r2/r2/public/static/js/ui.datepicker.js create mode 100644 r2/r2/public/static/reddit_ban.png create mode 100644 r2/r2/public/static/reddit_edit.png create mode 100644 r2/r2/public/static/reddit_reported.png create mode 100644 r2/r2/public/static/reddit_spam.png create mode 100644 r2/r2/public/static/reddit_traffic.png create mode 100755 r2/r2/public/static/redditaddict/appsbar/barbg.png create mode 100755 r2/r2/public/static/redditaddict/appsbar/buttonbg.png create mode 100755 r2/r2/public/static/redditaddict/appsbar/nub.png create mode 100755 r2/r2/public/static/redditaddict/badge/AIRInstallBadge.swf create mode 100755 r2/r2/public/static/redditaddict/badge/badgeimage.jpg create mode 100755 r2/r2/public/static/redditaddict/badge/expressinstall.swf create mode 100755 r2/r2/public/static/redditaddict/badge/swfobject.js create mode 100755 r2/r2/public/static/redditaddict/images/arrows.png create mode 100755 r2/r2/public/static/redditaddict/images/bg.png create mode 100755 r2/r2/public/static/redditaddict/images/black50.png create mode 100755 r2/r2/public/static/redditaddict/images/c-caps.png create mode 100755 r2/r2/public/static/redditaddict/images/c-tile.png create mode 100755 r2/r2/public/static/redditaddict/images/close.png create mode 100755 r2/r2/public/static/redditaddict/images/reddit-head.png create mode 100755 r2/r2/public/static/redditaddict/images/ss-graph.jpg create mode 100755 r2/r2/public/static/redditaddict/images/ss-main.jpg create mode 100755 r2/r2/public/static/redditaddict/images/star-blue.png create mode 100755 r2/r2/public/static/redditaddict/images/star-orangered.png create mode 100755 r2/r2/public/static/redditaddict/images/star-white.png create mode 100755 r2/r2/public/static/redditaddict/images/support.png create mode 100755 r2/r2/public/static/redditaddict/images/thumbsup3.png create mode 100755 r2/r2/public/static/redditaddict/images/w-bot.png create mode 100755 r2/r2/public/static/redditaddict/images/w-mid.png create mode 100755 r2/r2/public/static/redditaddict/images/w-top.png create mode 100755 r2/r2/public/static/redditaddict/index.html create mode 100755 r2/r2/public/static/socialite/appsbar/barbg.png create mode 100755 r2/r2/public/static/socialite/appsbar/buttonbg.png create mode 100755 r2/r2/public/static/socialite/appsbar/nub.png mode change 100644 => 100755 r2/r2/public/static/socialite/socialitelogo.png create mode 100644 r2/r2/templates/adminawardgive.html create mode 100644 r2/r2/templates/adminawards.html rename r2/r2/templates/{userstats.html => adminawardwinners.html} (51%) create mode 100644 r2/r2/templates/dart_ad.html create mode 100644 r2/r2/templates/paymentform.html create mode 100644 r2/r2/templates/promo_email.email create mode 100644 r2/r2/templates/promote_graph.html delete mode 100644 r2/r2/templates/promotedlinks.html create mode 100644 r2/r2/templates/selfserveblurb.html create mode 100644 r2/r2/templates/sidecontentbox.html create mode 100644 r2/r2/templates/sponsorshipbox.html create mode 100644 r2/r2/templates/takedownpane.html create mode 100644 r2/r2/templates/trophycase.html create mode 100644 r2/r2/templates/userawards.html create mode 100644 r2/r2/templates/verifyemail.email create mode 100644 r2/r2/templates/wrappeduser.html diff --git a/r2/Makefile b/r2/Makefile index 885431292..90e849960 100644 --- a/r2/Makefile +++ b/r2/Makefile @@ -21,11 +21,14 @@ ################################################################################ # Jacascript files to be compressified -js_targets = jquery.js jquery.json.js jquery.reddit.js reddit.js +js_targets = jquery.js jquery.json.js jquery.reddit.js reddit.js ui.core.js ui.datepicker.js sponsored.js # CSS targets -css_targets = reddit.css reddit-ie6-hax.css reddit-ie7-hax.css mobile.css spreadshirt.css +main_css = reddit.css +css_targets = reddit-ie6-hax.css reddit-ie7-hax.css mobile.css spreadshirt.css SED=sed +CAT=cat +CSS_COMPRESS = $(SED) -e 's/ \+/ /' -e 's/\/\*.*\*\///g' -e 's/: /:/' | grep -v "^ *$$" package = r2 static_dir = $(package)/public/static @@ -41,7 +44,8 @@ PRIVATEREPOS = $(shell python -c 'exec "try: import r2admin; print r2admin.__pat JSTARGETS := $(foreach js, $(js_targets), $(static_dir)/$(js)) CSSTARGETS := $(foreach css, $(css_targets), $(static_dir)/$(css)) -RTLCSS = $(CSSTARGETS:.css=-rtl.css) +MAINCSS := $(foreach css, $(main_css), $(static_dir)/$(css)) +RTLCSS = $(CSSTARGETS:.css=-rtl.css) $(MAINCSS:.css=-rtl.css) MD5S = $(JSTARGETS:=.md5) $(CSSTARGETS:=.md5) @@ -67,10 +71,10 @@ $(JSTARGETS): $(static_dir)/%.js : $(static_dir)/js/%.js $(JSCOMPRESS) < $< > $@ $(CSSTARGETS): $(static_dir)/%.css : $(static_dir)/css/%.css - $(SED) -e 's/ \+/ /' \ - -e 's/\/\*.*\*\///g' \ - -e 's/: /:/' \ - $< | grep -v "^ *$$" > $@ + $(CAT) $< | $(CSS_COMPRESS) > $@ + +$(MAINCSS): $(static_dir)/%.css : $(static_dir)/css/%.css + python r2/lib/contrib/nymph.py $< | $(CSS_COMPRESS) > $@ $(RTLCSS): %-rtl.css : %.css $(SED) -e "s/left/>#### 2)): + item.editted = True + item._commit() tc.changed(item) @@ -591,7 +606,8 @@ class ApiController(RedditController): link = Link._byID(parent.link_id, data = True) parent_comment = parent sr = parent.subreddit_slow - if not sr.should_ratelimit(c.user, 'comment'): + if ((link.is_self and link.author_id == c.user._id) + or not sr.should_ratelimit(c.user, 'comment')): should_ratelimit = False #remove the ratelimit error if the user's karma is high @@ -616,12 +632,13 @@ class ApiController(RedditController): comment, ip) item.parent_id = parent._id else: - item, inbox_rel = Comment._new(c.user, link, parent_comment, - comment, ip) + item, inbox_rel = Comment._new(c.user, link, parent_comment, + comment, ip) Vote.vote(c.user, item, True, ip) - # flag search indexer that something has changed - tc.changed(item) - + + # will also update searchchanges as appropriate + worker.do(lambda: amqp.add_item('new_comment', item._fullname)) + #update last modified set_last_modified(c.user, 'overview') set_last_modified(c.user, 'commented') @@ -724,7 +741,7 @@ class ApiController(RedditController): def POST_vote(self, dir, thing, ip, vote_type): ip = request.ip user = c.user - if not thing: + if not thing or thing._deleted: return # TODO: temporary hack until we migrate the rest of the vote data @@ -732,6 +749,8 @@ class ApiController(RedditController): g.log.debug("POST_vote: ignoring old vote on %s" % thing._fullname) return + # in a lock to prevent duplicate votes from people + # double-clicking the arrows with g.make_lock('vote_lock(%s,%s)' % (c.user._id36, thing._id36)): dir = (True if dir > 0 else False if dir < 0 @@ -846,26 +865,30 @@ class ApiController(RedditController): return self.abort(403,'forbidden') c.site.del_image(name) c.site._commit() - + @validatedForm(VSrModerator(), - VModhash()) - def POST_delete_sr_header(self, form, jquery): + VModhash(), + sponsor = VInt("sponsor", min = 0, max = 1)) + def POST_delete_sr_header(self, form, jquery, sponsor): """ 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: + if sponsor and c.user_is_admin: + c.site.sponsorship_img = None + c.site._commit() + elif c.site.header: + # reset the header image on the page + jquery('#header-img').attr("src", DefaultSR.header) c.site.header = None c.site._commit() - # reset the header image on the page - form.find('#header-img').attr("src", DefaultSR.header) # hide the button which started this - form.find('#delete-img').hide() + form.find('.delete-img').hide() # hide the preview box - form.find('#img-preview-container').hide() + form.find('.img-preview-container').hide() # reset the status boxes form.set_html('.img-status', _("deleted")) @@ -885,8 +908,10 @@ class ApiController(RedditController): VModhash(), file = VLength('file', max_length=1024*500), name = VCssName("name"), - header = nop('header')) - def POST_upload_sr_img(self, file, header, name): + form_id = VLength('formid', max_length = 100), + header = VInt('header', max=1, min=0), + sponsor = VInt('sponsor', max=1, min=0)) + def POST_upload_sr_img(self, file, header, sponsor, name, form_id): """ Called on /about/stylesheet when an image needs to be replaced or uploaded, as well as on /about/edit for updating the @@ -909,13 +934,16 @@ class ApiController(RedditController): try: cleaned = cssfilter.clean_image(file,'PNG') if header: - num = None # there is one and only header, and it is unnumbered + # there is one and only header, and it is unnumbered + resource = None + elif sponsor and c.user_is_admin: + resource = "sponsor" 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) + resource = c.site.add_image(name, max_num = g.max_sr_images) c.site._commit() except cssfilter.BadImage: @@ -931,15 +959,18 @@ class ApiController(RedditController): 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) + # while any other image will be ${c.site._fullname}_${resource}.png + new_url = cssfilter.save_sr_image(c.site, cleaned, + resource = resource) if header: c.site.header = new_url + elif sponsor and c.user_is_admin: + c.site.sponsorship_img = new_url c.site._commit() - + return UploadedImage(_('saved'), new_url, name, - errors = errors).render() - + errors = errors, form_id = form_id).render() + @validatedForm(VUser(), VModhash(), @@ -950,21 +981,27 @@ class ApiController(RedditController): name = VSubredditName("name"), title = VLength("title", max_length = 100), domain = VCnameDomain("domain"), - description = VLength("description", max_length = 500), + description = VLength("description", max_length = 1000), lang = VLang("lang"), over_18 = VBoolean('over_18'), show_media = VBoolean('show_media'), type = VOneOf('type', ('public', 'private', 'restricted')), ip = ValidIP(), + ad_type = VOneOf('ad', ('default', 'basic', 'custom')), + ad_file = VLength('ad-location', max_length = 500), + sponsor_name =VLength('sponsorship-name', max_length = 500), + sponsor_url = VLength('sponsorship-url', max_length = 500), + css_on_cname = VBoolean("css_on_cname"), ) - def POST_site_admin(self, form, jquery, name ='', ip = None, sr = None, **kw): + def POST_site_admin(self, form, jquery, name, ip, sr, ad_type, ad_file, + sponsor_url, sponsor_name, **kw): # the status button is outside the form -- have to reset by hand form.parent().set_html('.status', "") redir = False kw = dict((k, v) for k, v in kw.iteritems() if k in ('name', 'title', 'domain', 'description', 'over_18', - 'show_media', 'type', 'lang',)) + 'show_media', 'type', 'lang', "css_on_cname")) #if a user is banned, return rate-limit errors if c.user._spam: @@ -976,11 +1013,8 @@ class ApiController(RedditController): if cname_sr and (not sr or sr != cname_sr): c.errors.add(errors.USED_CNAME) - if not sr and form.has_errors(None, errors.RATELIMIT): - # this form is a little odd in that the error field - # doesn't occur within the form, so we need to manually - # set this text - form.parent().find('.RATELIMIT').html(c.errors[errors.RATELIMIT].message).show() + if not sr and form.has_errors("ratelimit", errors.RATELIMIT): + pass elif not sr and form.has_errors("name", errors.SUBREDDIT_EXISTS, errors.BAD_SR_NAME): form.find('#example_name').hide() @@ -996,13 +1030,17 @@ class ApiController(RedditController): #sending kw is ok because it was sanitized above sr = Subreddit._new(name = name, author_id = c.user._id, ip = ip, **kw) + + # will also update search + worker.do(lambda: amqp.add_item('new_subreddit', sr._fullname)) + Subreddit.subscribe_defaults(c.user) # make sure this user is on the admin list of that site! if sr.add_subscriber(c.user): sr._incr('_ups', 1) sr.add_moderator(c.user) sr.add_contributor(c.user) - redir = sr.path + "about/edit/?created=true" + redir = sr.path + "about/edit/?created=true" if not c.user_is_admin: VRatelimit.ratelimit(rate_user=True, rate_ip = True, @@ -1010,9 +1048,20 @@ class ApiController(RedditController): #editting an existing reddit elif sr.is_moderator(c.user) or c.user_is_admin: + + if c.user_is_admin: + sr.ad_type = ad_type + if ad_type != "custom": + ad_file = Subreddit._defaults['ad_file'] + sr.ad_file = ad_file + sr.sponsorship_url = sponsor_url or None + sr.sponsorship_name = sponsor_name or None + #assume sr existed, or was just built old_domain = sr.domain + if not sr.domain: + del kw['css_on_cname'] for k, v in kw.iteritems(): setattr(sr, k, v) sr._commit() @@ -1028,6 +1077,8 @@ class ApiController(RedditController): if redir: form.redirect(redir) + else: + jquery.refresh() @noresponse(VUser(), VModhash(), VSrCanBan('id'), @@ -1108,7 +1159,7 @@ class ApiController(RedditController): user = c.user if c.user_is_loggedin else None if not link or not link.subreddit_slow.can_view(user): return self.abort(403,'forbidden') - + if children: builder = CommentBuilder(link, CommentSortMenu.operator(sort), children) @@ -1124,7 +1175,7 @@ class ApiController(RedditController): cm.child = None else: items.append(cm.child) - + return items # assumes there is at least one child # a = _children(items[0].child.things) @@ -1191,10 +1242,11 @@ class ApiController(RedditController): return else: emailer.password_email(user) - form.set_html(".status", _("an email will be sent to that account's address shortly")) + form.set_html(".status", + _("an email will be sent to that account's address shortly")) - @validatedForm(cache_evt = VCacheKey('reset', ('key', 'name')), + @validatedForm(cache_evt = VCacheKey('reset', ('key',)), password = VPassword(['passwd', 'passwd2'])) def POST_resetpassword(self, form, jquery, cache_evt, password): if form.has_errors('name', errors.EXPIRED): @@ -1280,6 +1332,91 @@ class ApiController(RedditController): tr._is_enabled = True + @validatedForm(VAdmin(), + award = VByName("fullname"), + colliding_award=VAwardByCodename(("codename", "fullname")), + codename = VLength("codename", max_length = 100), + title = VLength("title", max_length = 100), + imgurl = VLength("imgurl", max_length = 1000)) + def POST_editaward(self, form, jquery, award, colliding_award, codename, + title, imgurl): + if form.has_errors(("codename", "title", "imgurl"), errors.NO_TEXT): + pass + + if form.has_errors(("codename"), errors.INVALID_OPTION): + form.set_html(".status", "some other award has that codename") + pass + + if form.has_error(): + return + + if award is None: + Award._new(codename, title, imgurl) + form.set_html(".status", "saved. reload to see it.") + return + + award.codename = codename + award.title = title + award.imgurl = imgurl + award._commit() + form.set_html(".status", _('saved')) + + @validatedForm(VAdmin(), + award = VByName("fullname"), + description = VLength("description", max_length=1000), + url = VLength("url", max_length=1000), + cup_hours = VFloat("cup_hours", + coerce=False, min=0, max=24 * 365), + recipient = VExistingUname("recipient")) + def POST_givetrophy(self, form, jquery, award, description, + url, cup_hours, recipient): + if form.has_errors("award", errors.NO_TEXT): + pass + + if form.has_errors("recipient", errors.USER_DOESNT_EXIST): + pass + + if form.has_errors("recipient", errors.NO_USER): + pass + + if form.has_errors("fullname", errors.NO_TEXT): + pass + + if form.has_errors("cup_hours", errors.BAD_NUMBER): + pass + + if form.has_error(): + return + + if cup_hours: + cup_seconds = int(cup_hours * 3600) + cup_expiration = timefromnow("%s seconds" % cup_seconds) + else: + cup_expiration = None + + t = Trophy._new(recipient, award, description=description, + url=url, cup_expiration=cup_expiration) + + form.set_html(".status", _('saved')) + + @validatedForm(VAdmin(), + account = VExistingUname("account")) + def POST_removecup(self, form, jquery, account): + if not account: + return self.abort404() + account.remove_cup() + + @validatedForm(VAdmin(), + trophy = VTrophy("trophy_fn")) + def POST_removetrophy(self, form, jquery, trophy): + if not trophy: + return self.abort404() + recipient = trophy._thing1 + award = trophy._thing2 + trophy._delete() + Trophy.by_account(recipient, _update=True) + Trophy.by_award(award, _update=True) + @validatedForm(links = VByName('links', thing_cls = Link, multiple = True), show = VByName('show', thing_cls = Link, multiple = False)) def POST_fetch_links(self, form, jquery, links, show): @@ -1300,113 +1437,6 @@ class ApiController(RedditController): setattr(c.user, "pref_" + ui_elem, False) c.user._commit() - @noresponse(VSponsor(), - thing = VByName('id')) - def POST_unpromote(self, thing): - if not thing: return - unpromote(thing) - - @validatedForm(VSponsor(), - ValidDomain('url'), - ip = ValidIP(), - l = VLink('link_id'), - title = VTitle('title'), - url = VUrl(['url', 'sr'], allow_self = False), - sr = VSubmitSR('sr'), - subscribers_only = VBoolean('subscribers_only'), - disable_comments = VBoolean('disable_comments'), - expire = VOneOf('expire', ['nomodify', - 'expirein', 'cancel']), - timelimitlength = VInt('timelimitlength',1,1000), - timelimittype = VOneOf('timelimittype', - ['hours','days','weeks'])) - def POST_edit_promo(self, form, jquery, ip, - title, url, sr, subscribers_only, - disable_comments, - expire = None, - timelimitlength = None, timelimittype = None, - l = None): - if isinstance(url, str): - # VUrl may have modified the URL to make it valid, like - # adding http:// - form.set_input('url', url) - elif isinstance(url, tuple) and isinstance(url[0], Link): - # there's already one or more links with this URL, but - # we're allowing mutliple submissions, so we really just - # want the URL - url = url[0].url - if form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG): - pass - elif form.has_errors('url', errors.NO_URL, errors.BAD_URL): - pass - elif ( (not l or url != l.url) and - form.has_errors('url', errors.NO_URL, errors.ALREADY_SUB) ): - #if url == l.url, we're just editting something else - pass - elif form.has_errors('sr', errors.SUBREDDIT_NOEXIST, - errors.SUBREDDIT_NOTALLOWED): - pass - elif (expire == 'expirein' and - form.has_errors('timelimitlength', errors.BAD_NUMBER)): - pass - elif l: - l.title = title - old_url = l.url - l.url = url - l.is_self = False - - l.promoted_subscribersonly = subscribers_only - l.disable_comments = disable_comments - - if expire == 'cancel': - l.promote_until = None - elif expire == 'expirein' and timelimitlength and timelimittype: - l.promote_until = timefromnow("%d %s" % (timelimitlength, - timelimittype)) - l._commit() - l.update_url_cache(old_url) - - form.redirect('/promote/edit_promo/%s' % to36(l._id)) - else: - l = Link._submit(title, url, c.user, sr, ip) - - if expire == 'expirein' and timelimitlength and timelimittype: - promote_until = timefromnow("%d %s" % (timelimitlength, - timelimittype)) - else: - promote_until = None - - l._commit() - - promote(l, subscribers_only = subscribers_only, - promote_until = promote_until, - disable_comments = disable_comments) - - form.redirect('/promote/edit_promo/%s' % to36(l._id)) - - def GET_link_thumb(self, *a, **kw): - """ - See GET_upload_sr_image for rationale - """ - return "nothing to see here." - - @validate(VSponsor(), - link = VByName('link_id'), - file = VLength('file', 500*1024)) - def POST_link_thumb(self, link=None, file=None): - errors = dict(BAD_CSS_NAME = "", IMAGE_ERROR = "") - try: - force_thumbnail(link, file) - except cssfilter.BadImage: - # if the image doesn't clean up nicely, abort - errors["IMAGE_ERROR"] = _("bad image") - - if any(errors.values()): - return UploadedImage("", "", "upload", errors = errors).render() - else: - return UploadedImage(_('saved'), thumbnail_url(link), "", - errors = errors).render() - @validatedForm(type = VOneOf('type', ('click'), default = 'click'), links = VByName('ids', thing_cls = Link, multiple = True)) def GET_gadget(self, form, jquery, type, links): @@ -1434,26 +1464,33 @@ class ApiController(RedditController): c.user.pref_frame_commentspanel = False c.user._commit() - @validatedForm(promoted = VByName('ids', thing_cls = Link, multiple = True)) - def POST_onload(self, form, jquery, promoted, *a, **kw): - if not promoted: - return - - # make sure that they are really promoted - promoted = [ l for l in promoted if l.promoted ] - - for l in promoted: - dest = l.url - + @validatedForm(promoted = VByName('ids', thing_cls = Link, + multiple = True), + sponsorships = VByName('ids', thing_cls = Subreddit, + multiple = True)) + def POST_onload(self, form, jquery, promoted, sponsorships, *a, **kw): + def add_tracker(dest, where, what): jquery.set_tracker( - l._fullname, - tracking.PromotedLinkInfo.gen_url(fullname=l._fullname, + where, + tracking.PromotedLinkInfo.gen_url(fullname=what, ip = request.ip), - tracking.PromotedLinkClickInfo.gen_url(fullname = l._fullname, + tracking.PromotedLinkClickInfo.gen_url(fullname = what, dest = dest, ip = request.ip) ) + if promoted: + # make sure that they are really promoted + promoted = [ l for l in promoted if l.promoted ] + for l in promoted: + add_tracker(l.url, l._fullname, l._fullname) + + if sponsorships: + for s in sponsorships: + add_tracker(s.sponsorship_url, s._fullname, + "%s_%s" % (s._fullname, s.sponsorship_name)) + + @json_validate(query = nop('query')) def POST_search_reddit_names(self, query): names = [] @@ -1469,7 +1506,7 @@ class ApiController(RedditController): wrapped = wrap_links(link) wrapped = list(wrapped)[0] - return spaceCompress(websafe(wrapped.link_child.content())) + return websafe(spaceCompress(wrapped.link_child.content())) @validatedForm(link = VByName('name', thing_cls = Link, multiple = False), color = VOneOf('color', spreadshirt.ShirtPane.colors), diff --git a/r2/r2/controllers/awards.py b/r2/r2/controllers/awards.py new file mode 100644 index 000000000..b4f68aafa --- /dev/null +++ b/r2/r2/controllers/awards.py @@ -0,0 +1,54 @@ +# The contents of this file are subject to the Common Public Attribution +# License Version 1.0. (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +# License Version 1.1, but Sections 14 and 15 have been added to cover use of +# software over a computer network and provide for limited attribution for the +# Original Developer. In addition, Exhibit A has been modified to be consistent +# with Exhibit B. +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +# the specific language governing rights and limitations under the License. +# +# The Original Code is Reddit. +# +# The Original Developer is the Initial Developer. The Initial Developer of the +# Original Code is CondeNet, Inc. +# +# All portions of the code written by CondeNet are Copyright (c) 2006-2009 +# CondeNet, Inc. All Rights Reserved. +################################################################################ +from pylons import request, g +from reddit_base import RedditController +from r2.lib.pages import AdminPage, AdminAwards +from r2.lib.pages import AdminAwardGive, AdminAwardWinners +from validator import * + +class AwardsController(RedditController): + + @validate(VAdmin()) + def GET_index(self): + res = AdminPage(content = AdminAwards(), + title = 'awards').render() + return res + + @validate(VAdmin(), + award = VAwardByCodename('awardcn')) + def GET_give(self, award): + if award is None: + abort(404, 'page not found') + + res = AdminPage(content = AdminAwardGive(award), + title='give an award').render() + return res + + @validate(VAdmin(), + award = VAwardByCodename('awardcn')) + def GET_winners(self, award): + if award is None: + abort(404, 'page not found') + + res = AdminPage(content = AdminAwardWinners(award), + title='award winners').render() + return res diff --git a/r2/r2/controllers/embed.py b/r2/r2/controllers/embed.py index 335b908f3..1fc4ec83e 100644 --- a/r2/r2/controllers/embed.py +++ b/r2/r2/controllers/embed.py @@ -44,16 +44,18 @@ class EmbedController(RedditController): # Add "edit this page" link if the user is allowed to edit the wiki if c.user_is_loggedin and c.user.can_wiki(): edit_text = _('edit this page') - read_first = _('read this first') + yes_you_can = _("yes, it's okay!") + read_first = _('just read this first.') url = "http://code.reddit.com/wiki" + websafe(fp) + "?action=edit" edittag = """ - """ % (url, edit_text, read_first) + """ % (url, edit_text, yes_you_can, read_first) output.append(edittag) @@ -69,6 +71,8 @@ class EmbedController(RedditController): fp = request.path.rstrip("/") u = "http://code.reddit.com/wiki" + fp + '?stripped=1' + g.log.debug("Pulling %s for help" % u) + try: content = proxyurl(u) return self.rendercontent(content, fp) diff --git a/r2/r2/controllers/error.py b/r2/r2/controllers/error.py index 5d591981b..6a818330d 100644 --- a/r2/r2/controllers/error.py +++ b/r2/r2/controllers/error.py @@ -33,6 +33,7 @@ try: # the stack trace won't be presented to the user in production from reddit_base import RedditController from r2.models.subreddit import Default, Subreddit + from r2.models.link import Link from r2.lib import pages from r2.lib.strings import strings, rand_strings except Exception, e: @@ -45,7 +46,7 @@ except Exception, e: # kill this app import os os._exit(1) - + redditbroke = \ ''' @@ -131,10 +132,14 @@ class ErrorController(RedditController): code = request.GET.get('code', '') srname = request.GET.get('srname', '') + takedown = request.GET.get('takedown', "") if srname: c.site = Subreddit._by_name(srname) if c.render_style not in self.allowed_render_styles: return str(code) + elif takedown and code == '404': + link = Link._by_fullname(takedown) + return pages.TakedownPage(link).render() elif code == '403': return self.send403() elif code == '500': diff --git a/r2/r2/controllers/errors.py b/r2/r2/controllers/errors.py index aa730268b..8791ae9bb 100644 --- a/r2/r2/controllers/errors.py +++ b/r2/r2/controllers/errors.py @@ -25,6 +25,7 @@ from copy import copy error_list = dict(( ('USER_REQUIRED', _("please login to do that")), + ('VERIFIED_USER_REQUIRED', _("you need to set a valid email address to do that.")), ('NO_URL', _('a url is required')), ('BAD_URL', _('you should check that url')), ('BAD_CAPTCHA', _('care to try these again?')), @@ -45,7 +46,8 @@ error_list = dict(( ('USER_DOESNT_EXIST', _("that user doesn't exist")), ('NO_USER', _('please enter a username')), ('INVALID_PREF', "that preference isn't valid"), - ('BAD_NUMBER', _("that number isn't in the right range")), + ('BAD_NUMBER', _("that number isn't in the right range (%(min)d to %(max)d)")), + ('BAD_BID', _("your bid must be at least $%(min)d per day and no more than to $%(max)d in total.")), ('ALREADY_SUB', _("that link has already been submitted")), ('SUBREDDIT_EXISTS', _('that reddit already exists')), ('SUBREDDIT_NOEXIST', _('that reddit doesn\'t exist')), @@ -64,7 +66,12 @@ error_list = dict(( ('BAD_EMAILS', _('the following emails are invalid: %(emails)s')), ('NO_EMAILS', _('please enter at least one email address')), ('TOO_MANY_EMAILS', _('please only share to %(num)s emails at a time.')), - + ('BAD_DATE', _('please provide a date of the form mm/dd/yyyy')), + ('BAD_DATE_RANGE', _('the dates need to be in order and not identical')), + ('BAD_FUTURE_DATE', _('please enter a date at least %(day)s days in the future')), + ('BAD_PAST_DATE', _('please enter a date at least %(day)s days in the past')), + ('BAD_ADDRESS', _('address problem: %(message)s')), + ('BAD_CARD', _('card problem: %(message)s')), ('TOO_LONG', _("this is too long (max: %(max_length)s)")), ('NO_TEXT', _('we need something here')), )) @@ -123,3 +130,4 @@ class ErrorSet(object): del self.errors[pair] class UserRequiredException(Exception): pass +class VerifiedUserRequiredException(Exception): pass diff --git a/r2/r2/controllers/feedback.py b/r2/r2/controllers/feedback.py index fb123289a..0f27e1b06 100644 --- a/r2/r2/controllers/feedback.py +++ b/r2/r2/controllers/feedback.py @@ -22,20 +22,26 @@ from reddit_base import RedditController from pylons import c, request from pylons.i18n import _ -from r2.lib.pages import FormPage, Feedback, Captcha +from r2.lib.pages import FormPage, Feedback, Captcha, PaneStack, SelfServeBlurb class FeedbackController(RedditController): def GET_ad_inq(self): title = _("inquire about advertising on reddit") return FormPage('advertise', - content = Feedback(title=title, - action='ad_inq'), + content = PaneStack([SelfServeBlurb(), + Feedback(title=title, + action='ad_inq')]), loginbox = False).render() def GET_feedback(self): title = _("send reddit feedback") return FormPage('feedback', - content = Feedback(title=title, - action='feedback'), + content = Feedback(title=title, action='feedback'), + loginbox = False).render() + + def GET_i18n(self): + title = _("help translate reddit into your language") + return FormPage('help translate', + content = Feedback(title=title, action='i18n'), loginbox = False).render() diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py index a0aa8a5a2..1d0f2eb69 100644 --- a/r2/r2/controllers/front.py +++ b/r2/r2/controllers/front.py @@ -102,7 +102,46 @@ class FrontController(RedditController): """The 'what is my password' page""" return BoringPage(_("password"), content=Password()).render() - @validate(cache_evt = VCacheKey('reset', ('key', 'name')), + @validate(VUser(), + dest = VDestination()) + def GET_verify(self, dest): + if c.user.email_verified: + content = InfoBar(message = strings.email_verified) + if dest: + return self.redirect(dest) + else: + content = PaneStack( + [InfoBar(message = strings.verify_email), + PrefUpdate(email = True, verify = True, + password = False)]) + return BoringPage(_("verify email"), content = content).render() + + @validate(VUser(), + cache_evt = VCacheKey('email_verify', ('key',)), + key = nop('key'), + dest = VDestination(default = "/prefs/update")) + def GET_verify_email(self, cache_evt, key, dest): + if c.user_is_loggedin and c.user.email_verified: + cache_evt.clear() + return self.redirect(dest) + elif not (cache_evt.user and + key == passhash(cache_evt.user.name, cache_evt.user.email)): + content = PaneStack( + [InfoBar(message = strings.email_verify_failed), + PrefUpdate(email = True, verify = True, + password = False)]) + return BoringPage(_("verify email"), content = content).render() + elif c.user != cache_evt.user: + # wrong user. Log them out and try again. + self.logout() + return self.redirect(request.fullpath) + else: + cache_evt.clear() + c.user.email_verified = True + c.user._commit() + return self.redirect(dest) + + @validate(cache_evt = VCacheKey('reset', ('key',)), key = nop('key')) def GET_resetpassword(self, cache_evt, key): """page hit once a user has been sent a password reset email @@ -129,11 +168,13 @@ class FrontController(RedditController): """The (now depricated) details page. Content on this page has been subsubmed by the presence of the LinkInfoBar on the rightbox, so it is only useful for Admin-only wizardry.""" - return DetailsPage(link = article).render() - + return DetailsPage(link = article, expand_children=False).render() + @validate(article = VLink('article')) def GET_shirt(self, article): + if not can_view_link_comments(article): + abort(403, 'forbidden') if g.spreadshirt_url: from r2.lib.spreadshirt import ShirtPage return ShirtPage(link = article).render() @@ -147,12 +188,18 @@ class FrontController(RedditController): def GET_comments(self, article, comment, context, sort, num_comments): """Comment page for a given 'article'.""" if comment and comment.link_id != article._id: - return self.abort404() - - if not c.default_sr and c.site._id != article.sr_id: return self.abort404() - - if not article.subreddit_slow.can_view(c.user): + + sr = Subreddit._byID(article.sr_id, True) + + if sr.name == g.takedown_sr: + request.environ['REDDIT_TAKEDOWN'] = article._fullname + return self.abort404() + + if not c.default_sr and c.site._id != sr._id: + return self.abort404() + + if not can_view_link_comments(article): abort(403, 'forbidden') #check for 304 @@ -188,8 +235,7 @@ class FrontController(RedditController): displayPane.append(PermalinkMessage(article.make_permalink_slow())) # insert reply box only for logged in user - if c.user_is_loggedin and article.subreddit_slow.can_comment(c.user)\ - and not is_api(): + if c.user_is_loggedin and can_comment_link(article) and not is_api(): #no comment box for permalinks displayPane.append(UserText(item = article, creating = True, post_form = 'comment', @@ -200,7 +246,7 @@ class FrontController(RedditController): displayPane.append(listing.listing()) loc = None if c.focal_comment or context is not None else 'comments' - + res = LinkInfoPage(link = article, comment = comment, content = displayPane, subtitle = _("comments"), @@ -300,10 +346,10 @@ class FrontController(RedditController): return self.abort404() return EditReddit(content = pane).render() - - def GET_stats(self): - """The stats page.""" - return BoringPage(_("stats"), content = UserStats()).render() + + def GET_awards(self): + """The awards page.""" + return BoringPage(_("awards"), content = UserAwards()).render() # filter for removing punctuation which could be interpreted as lucene syntax related_replace_regex = re.compile('[?\\&|!{}+~^()":*-]+') @@ -315,6 +361,9 @@ class FrontController(RedditController): """Related page: performs a search using title of article as the search query.""" + if not can_view_link_comments(article): + abort(403, 'forbidden') + title = c.site.name + ((': ' + article.title) if hasattr(article, 'title') else '') query = self.related_replace_regex.sub(self.related_replace_with, @@ -335,8 +384,10 @@ class FrontController(RedditController): @base_listing @validate(article = VLink('article')) def GET_duplicates(self, article, num, after, reverse, count): - links = link_duplicates(article) + if not can_view_link_comments(article): + abort(403, 'forbidden') + links = link_duplicates(article) builder = IDBuilder([ link._fullname for link in links ], num = num, after = after, reverse = reverse, count = count, skip = False) @@ -492,6 +543,12 @@ class FrontController(RedditController): c.response.content = '' return c.response + @validate(VAdmin(), + comment = VCommentByID('comment_id')) + def GET_comment_by_id(self, comment): + href = comment.make_permalink_slow(context=5, anchor=True) + return self.redirect(href) + @validate(VUser(), VSRSubmitPage(), url = VRequired('url', None), @@ -609,15 +666,23 @@ class FrontController(RedditController): return self.abort404() - @validate(VSponsor(), + @validate(VTrafficViewer('article'), article = VLink('article')) def GET_traffic(self, article): - res = LinkInfoPage(link = article, + content = PromotedTraffic(article) + if c.render_style == 'csv': + c.response.content = content.as_csv() + return c.response + + return LinkInfoPage(link = article, comment = None, - content = PromotedTraffic(article)).render() - return res - + content = content).render() + @validate(VAdmin()) def GET_site_traffic(self): return BoringPage("traffic", content = RedditTraffic()).render() + + + def GET_ad(self, reddit = None): + return Dart_Ad(reddit).render(style="html") diff --git a/r2/r2/controllers/health.py b/r2/r2/controllers/health.py new file mode 100644 index 000000000..7c8a041df --- /dev/null +++ b/r2/r2/controllers/health.py @@ -0,0 +1,52 @@ +from threading import Thread +import os +import time + +from pylons.controllers.util import abort +from pylons import c, g + +from reddit_base import RedditController +from r2.lib.utils import worker + +class HealthController(RedditController): + def shutdown(self): + thread_pool = c.thread_pool + def _shutdown(): + #give busy threads 30 seconds to finish up + for s in xrange(30): + busy = thread_pool.track_threads()['busy'] + if not busy: + break + time.sleep(1) + + thread_pool.shutdown() + worker.join() + os._exit(3) + + t = Thread(target = _shutdown) + t.setDaemon(True) + t.start() + + def GET_health(self): + c.dontcache = True + + if g.shutdown: + if g.shutdown == 'init': + self.shutdown() + g.shutdown = 'shutdown' + abort(503, 'service temporarily unavailable') + else: + c.response_content_type = 'text/plain' + c.response.content = "i'm still alive!" + return c.response + + def GET_shutdown(self): + if not g.allow_shutdown: + self.abort404() + + c.dontcache = True + #the will make the next health-check initiate the shutdown + g.shutdown = 'init' + c.response_content_type = 'text/plain' + c.response.content = 'shutting down...' + return c.response diff --git a/r2/r2/controllers/listingcontroller.py b/r2/r2/controllers/listingcontroller.py index c16c4f8e7..a41061b7b 100644 --- a/r2/r2/controllers/listingcontroller.py +++ b/r2/r2/controllers/listingcontroller.py @@ -38,6 +38,7 @@ from r2.lib.jsontemplates import is_api from r2.lib.solrsearch import SearchQuery from r2.lib.utils import iters, check_cheating, timeago from r2.lib import sup +from r2.lib.promote import PromoteSR from admin import admin_profile_query @@ -97,7 +98,6 @@ class ListingController(RedditController): show_sidebar = self.show_sidebar, nav_menus = self.menus, title = self.title(), - infotext = self.infotext, **self.render_params).render() return res @@ -135,10 +135,17 @@ class ListingController(RedditController): return b def keep_fn(self): - return None + def keep(item): + wouldkeep = item.keep_item(item) + if getattr(item, "promoted", None) is not None: + return False + return wouldkeep + return keep def listing(self): """Listing to generate from the builder""" + if c.site.path == PromoteSR.path and not c.user_is_sponsor: + abort(403, 'forbidden') listing = LinkListing(self.builder_obj, show_nums = self.show_nums) return listing.listing() @@ -189,9 +196,12 @@ class HotController(FixListing, ListingController): if o_links: # get links in proximity to pos l = min(len(o_links) - 3, 8) - disp_links = [o_links[(i + pos) % len(o_links)] for i in xrange(-2, l)] - - b = IDBuilder(disp_links, wrap = self.builder_wrapper) + disp_links = [o_links[(i + pos) % len(o_links)] + for i in xrange(-2, l)] + def keep_fn(item): + return item.likes is None and item.keep_item(item) + b = IDBuilder(disp_links, wrap = self.builder_wrapper, + skip = True, keep_fn = keep_fn) o = OrganicListing(b, org_links = o_links, visible_link = o_links[pos], @@ -276,7 +286,10 @@ class NewController(ListingController): for things like the spam filter and thumbnail fetcher to act on them before releasing them into the wild""" wouldkeep = item.keep_item(item) - if c.user_is_loggedin and (c.user_is_admin or item.subreddit.is_moderator(c.user)): + if item.promoted is not None: + return False + elif c.user_is_loggedin and (c.user_is_admin or + item.subreddit.is_moderator(c.user)): # let admins and moderators see them regardless return wouldkeep elif wouldkeep and c.user_is_loggedin and c.user._id == item.author_id: @@ -379,7 +392,6 @@ class RecommendedController(ListingController): class UserController(ListingController): render_cls = ProfilePage - skip = False show_nums = False def title(self): @@ -393,6 +405,14 @@ class UserController(ListingController): % dict(user = self.vuser.name, site = c.site.name) return title + # TODO: this might not be the place to do this + skip = True + def keep_fn(self): + # keep promotions off of profile pages. + def keep(item): + return getattr(item, "promoted", None) is None + return keep + def query(self): q = None if self.where == 'overview': @@ -475,7 +495,6 @@ class MessageController(ListingController): w = Wrapped(thing) w.render_class = Message w.to_id = c.user._id - w.subject = _('comment reply') w.was_comment = True w.permalink, w._fullname = p, f return w diff --git a/r2/r2/controllers/post.py b/r2/r2/controllers/post.py index 83799e2d6..c9eee9d28 100644 --- a/r2/r2/controllers/post.py +++ b/r2/r2/controllers/post.py @@ -26,6 +26,7 @@ from r2.lib.emailer import opt_in, opt_out from pylons import request, c, g from validator import * from pylons.i18n import _ +from r2.models import * import sha def to_referer(func, **params): @@ -37,7 +38,7 @@ def to_referer(func, **params): class PostController(ApiController): - def response_func(self, kw): + def api_wrapper(self, kw): return Storage(**kw) #TODO: feature disabled for now @@ -103,11 +104,16 @@ class PostController(ApiController): pref_num_comments = VInt('num_comments', 1, g.max_comments, default = g.num_comments), pref_show_stylesheets = VBoolean('show_stylesheets'), + pref_show_promote = VBoolean('show_promote'), all_langs = nop('all-langs', default = 'all')) def POST_options(self, all_langs, pref_lang, **kw): #temporary. eventually we'll change pref_clickgadget to an #integer preference kw['pref_clickgadget'] = kw['pref_clickgadget'] and 5 or 0 + if c.user.pref_show_promote is None: + kw['pref_show_promote'] = None + elif not kw.get('pref_show_promote'): + kw['pref_show_promote'] = False self.set_options(all_langs, pref_lang, **kw) u = UrlParser(c.site.path + "prefs") @@ -115,14 +121,14 @@ class PostController(ApiController): if c.cname: u.put_in_frame() return self.redirect(u.unparse()) - + def GET_over18(self): return BoringPage(_("over 18?"), content = Over18()).render() @validate(over18 = nop('over18'), uh = nop('uh'), - dest = nop('dest')) + dest = VDestination(default = '/')) def POST_over18(self, over18, uh, dest): if over18 == 'yes': if c.user_is_loggedin and c.user.valid_hash(uh): @@ -199,3 +205,4 @@ class PostController(ApiController): def GET_login(self, *a, **kw): return self.redirect('/login' + query_string(dict(dest="/"))) + diff --git a/r2/r2/controllers/promotecontroller.py b/r2/r2/controllers/promotecontroller.py index 21653d9a2..e2c320052 100644 --- a/r2/r2/controllers/promotecontroller.py +++ b/r2/r2/controllers/promotecontroller.py @@ -22,6 +22,7 @@ from validator import * from pylons.i18n import _ from r2.models import * +from r2.lib.authorize import get_account_info, edit_profile from r2.lib.pages import * from r2.lib.pages.things import wrap_links from r2.lib.menus import * @@ -29,48 +30,413 @@ from r2.controllers import ListingController from r2.controllers.reddit_base import RedditController -from r2.lib.promote import get_promoted +from r2.lib.promote import get_promoted, STATUS, PromoteSR from r2.lib.utils import timetext - +from r2.lib.media import force_thumbnail, thumbnail_url +from r2.lib import cssfilter from datetime import datetime -class PromoteController(RedditController): - @validate(VSponsor()) - def GET_index(self): - return self.GET_current_promos() +class PromoteController(ListingController): + skip = False + where = 'promoted' + render_cls = PromotePage - @validate(VSponsor()) - def GET_current_promos(self): - render_list = list(wrap_links(get_promoted())) - for x in render_list: - if x.promote_until: - x.promote_expires = timetext(datetime.now(g.tz) - x.promote_until) - page = PromotePage('current_promos', - content = PromotedLinks(render_list)) + @property + def title_text(self): + return _('promoted by you') + + def query(self): + q = Link._query(Link.c.sr_id == PromoteSR._id) + if not c.user_is_sponsor: + # get user's own promotions + q._filter(Link.c.author_id == c.user._id) + q._filter(Link.c._spam == (True, False), + Link.c.promoted == (True, False)) + q._sort = desc('_date') + + if self.sort == "future_promos": + q._filter(Link.c.promote_status == STATUS.unseen) + elif self.sort == "pending_promos": + if c.user_is_admin: + q._filter(Link.c.promote_status == STATUS.pending) + else: + q._filter(Link.c.promote_status == (STATUS.unpaid, + STATUS.unseen, + STATUS.accepted, + STATUS.rejected)) + elif self.sort == "unpaid_promos": + q._filter(Link.c.promote_status == STATUS.unpaid) + elif self.sort == "live_promos": + q._filter(Link.c.promote_status == STATUS.promoted) + + return q + + @validate(VPaidSponsor(), + VVerifiedUser()) + def GET_listing(self, sort = "", **env): + self.sort = sort + return ListingController.GET_listing(self, **env) + + GET_index = GET_listing - return page.render() - - @validate(VSponsor()) + # To open up: VSponsor -> VVerifiedUser + @validate(VPaidSponsor(), + VVerifiedUser()) def GET_new_promo(self): - page = PromotePage('new_promo', - content = PromoteLinkForm()) - return page.render() + return PromotePage('content', content = PromoteLinkForm()).render() - @validate(VSponsor(), + @validate(VSponsor('link'), link = VLink('link')) def GET_edit_promo(self, link): - sr = Subreddit._byID(link.sr_id) - listing = wrap_links(link) - + if link.promoted is None: + return self.abort404() + rendered = wrap_links(link) timedeltatext = '' if link.promote_until: timedeltatext = timetext(link.promote_until - datetime.now(g.tz), resultion=2) - form = PromoteLinkForm(sr = sr, link = link, - listing = listing, + form = PromoteLinkForm(link = link, + listing = rendered, timedeltatext = timedeltatext) page = PromotePage('new_promo', content = form) return page.render() + @validate(VPaidSponsor(), + VVerifiedUser()) + def GET_graph(self): + content = Promote_Graph() + if c.user_is_sponsor and c.render_style == 'csv': + c.response.content = content.as_csv() + return c.response + return PromotePage("grpaph", content = content).render() + + + ### POST controllers below + @validatedForm(VSponsor(), + link = VByName("link"), + bid = VBid('bid', "link")) + def POST_freebie(self, form, jquery, link, bid): + if link and link.promoted is not None and bid: + promote.auth_paid_promo(link, c.user, -1, bid) + jquery.refresh() + + @validatedForm(VSponsor(), + link = VByName("link"), + note = nop("note")) + def POST_promote_note(self, form, jquery, link, note): + if link and link.promoted is not None: + form.find(".notes").children(":last").after( + "

" + promote.promotion_log(link, note, True) + "

") + + + @validatedForm(VSponsor(), + link = VByName("link"), + refund = VFloat("refund")) + def POST_refund(self, form, jquery, link, refund): + if link: + # make sure we don't refund more than we should + author = Account._byID(link.author_id) + promote.refund_promo(link, author, refund) + jquery.refresh() + + @noresponse(VSponsor(), + thing = VByName('id')) + def POST_promote(self, thing): + if thing: + now = datetime.now(g.tz) + # make accepted if unseen or already rejected + if thing.promote_status in (promote.STATUS.unseen, + promote.STATUS.rejected): + promote.accept_promo(thing) + # if not finished and the dates are current + elif (thing.promote_status < promote.STATUS.finished and + thing._date <= now and thing.promote_until > now): + # if already pending, cron job must have failed. Promote. + if thing.promote_status == promote.STATUS.accepted: + promote.pending_promo(thing) + promote.promote(thing) + + @noresponse(VSponsor(), + thing = VByName('id'), + reason = nop("reason")) + def POST_unpromote(self, thing, reason): + if thing: + if (c.user_is_sponsor and + (thing.promote_status in (promote.STATUS.unpaid, + promote.STATUS.unseen, + promote.STATUS.accepted, + promote.STATUS.promoted)) ): + promote.reject_promo(thing, reason = reason) + else: + promote.unpromote(thing) + + # TODO: when opening up, may have to refactor + @validatedForm(VPaidSponsor('link_id'), + VModhash(), + VRatelimit(rate_user = True, + rate_ip = True, + prefix = 'create_promo_'), + ip = ValidIP(), + l = VLink('link_id'), + title = VTitle('title'), + url = VUrl('url', allow_self = False), + dates = VDateRange(['startdate', 'enddate'], + future = g.min_promote_future, + reference_date = promote.promo_datetime_now, + business_days = True, + admin_override = True), + disable_comments = VBoolean("disable_comments"), + set_clicks = VBoolean("set_maximum_clicks"), + max_clicks = VInt("maximum_clicks", min = 0), + set_views = VBoolean("set_maximum_views"), + max_views = VInt("maximum_views", min = 0), + bid = VBid('bid', 'link_id')) + def POST_new_promo(self, form, jquery, l, ip, title, url, dates, + disable_comments, + set_clicks, max_clicks, set_views, max_views, bid): + should_ratelimit = False + if not c.user_is_sponsor: + set_clicks = False + set_views = False + should_ratelimit = True + if not set_clicks: + max_clicks = None + if not set_views: + max_views = None + + if not should_ratelimit: + c.errors.remove((errors.RATELIMIT, 'ratelimit')) + + # demangle URL in canonical way + if url: + if isinstance(url, (unicode, str)): + form.set_inputs(url = url) + elif isinstance(url, tuple) or isinstance(url[0], Link): + # there's already one or more links with this URL, but + # we're allowing mutliple submissions, so we really just + # want the URL + url = url[0].url + + # check dates and date range + start, end = [x.date() for x in dates] if dates else (None, None) + if not l or (l._date.date(), l.promote_until.date()) == (start,end): + if (form.has_errors('startdate', errors.BAD_DATE, + errors.BAD_FUTURE_DATE) or + form.has_errors('enddate', errors.BAD_DATE, + errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)): + return + + # dates have been validated at this point. Next validate title, etc. + if (form.has_errors('title', errors.NO_TEXT, + errors.TOO_LONG) or + form.has_errors('url', errors.NO_URL, errors.BAD_URL) or + form.has_errors('bid', errors.BAD_BID) or + (not l and jquery.has_errors('ratelimit', errors.RATELIMIT))): + return + elif l: + if l.promote_status == promote.STATUS.finished: + form.parent().set_html(".status", + _("that promoted link is already finished.")) + else: + # we won't penalize for changes of dates provided + # the submission isn't pending (or promoted, or + # finished) + changed = False + if dates and not promote.update_promo_dates(l, *dates): + form.parent().set_html(".status", + _("too late to change the date.")) + else: + changed = True + + # check for changes in the url and title + if promote.update_promo_data(l, title, url): + changed = True + # sponsors can change the bid value (at the expense of making + # the promotion a freebie) + if c.user_is_sponsor and bid != l.promote_bid: + promote.auth_paid_promo(l, c.user, -1, bid) + promote.accept_promo(l) + changed = True + + if c.user_is_sponsor: + l.maximum_clicks = max_clicks + l.maximum_views = max_views + changed = True + + l.disable_comments = disable_comments + l._commit() + + if changed: + jquery.refresh() + + # no link so we are creating a new promotion + elif dates: + promote_start, promote_end = dates + # check that the bid satisfies the minimum + duration = max((promote_end - promote_start).days, 1) + if bid / duration >= g.min_promote_bid: + l = promote.new_promotion(title, url, c.user, ip, + promote_start, promote_end, bid, + disable_comments = disable_comments, + max_clicks = max_clicks, + max_views = max_views) + # if the submitter is a sponsor (or implicitly an admin) we can + # fast-track the approval and auto-accept the bid + if c.user_is_sponsor: + promote.auth_paid_promo(l, c.user, -1, bid) + promote.accept_promo(l) + + # register a vote + v = Vote.vote(c.user, l, True, ip) + + # set the rate limiter + if should_ratelimit: + VRatelimit.ratelimit(rate_user=True, rate_ip = True, + prefix = "create_promo_", + seconds = 60) + + form.redirect(promote.promo_edit_url(l)) + else: + c.errors.add(errors.BAD_BID, + msg_params = dict(min=g.min_promote_bid, + max=g.max_promote_bid), + field = 'bid') + form.set_error(errors.BAD_BID, "bid") + + @validatedForm(VSponsor('container'), + VModhash(), + user = VExistingUname('name'), + thing = VByName('container')) + def POST_traffic_viewer(self, form, jquery, user, thing): + """ + Adds a user to the list of users allowed to view a promoted + link's traffic page. + """ + if not form.has_errors("name", + errors.USER_DOESNT_EXIST, errors.NO_USER): + form.set_inputs(name = "") + form.set_html(".status:first", _("added")) + if promote.add_traffic_viewer(thing, user): + user_row = TrafficViewerList(thing).user_row(user) + jquery("#traffic-table").show( + ).find("table").insert_table_rows(user_row) + + # send the user a message + msg = strings.msg_add_friend.get("traffic") + subj = strings.subj_add_friend.get("traffic") + if msg and subj: + d = dict(url = thing.make_permalink_slow(), + traffic_url = promote.promo_traffic_url(thing), + title = thing.title) + msg = msg % d + subk =msg % d + item, inbox_rel = Message._new(c.user, user, + subj, msg, request.ip) + if g.write_query_queue: + queries.new_message(item, inbox_rel) + + + @validatedForm(VSponsor('container'), + VModhash(), + iuser = VByName('id'), + thing = VByName('container')) + def POST_rm_traffic_viewer(self, form, jquery, iuser, thing): + if thing and iuser: + promote.rm_traffic_viewer(thing, iuser) + + + @validatedForm(VSponsor('link'), + link = VByName("link"), + customer_id = VInt("customer_id", min = 0), + bid = VBid("bid", "link"), + pay_id = VInt("account", min = 0), + edit = VBoolean("edit"), + address = ValidAddress(["firstName", "lastName", + "company", "address", + "city", "state", "zip", + "country", "phoneNumber"], + usa_only = True), + creditcard = ValidCard(["cardNumber", "expirationDate", + "cardCode"])) + def POST_update_pay(self, form, jquery, bid, link, customer_id, pay_id, + edit, address, creditcard): + address_modified = not pay_id or edit + if address_modified: + if (form.has_errors(["firstName", "lastName", "company", "address", + "city", "state", "zip", + "country", "phoneNumber"], + errors.BAD_ADDRESS) or + form.has_errors(["cardNumber", "expirationDate", "cardCode"], + errors.BAD_CARD)): + pass + else: + pay_id = edit_profile(c.user, address, creditcard, pay_id) + if form.has_errors('bid', errors.BAD_BID) or not bid: + pass + # if link is in use or finished, don't make a change + elif link.promote_status == promote.STATUS.promoted: + form.set_html(".status", + _("that link is currently promoted. " + "you can't update your bid now.")) + elif link.promote_status == promote.STATUS.finished: + form.set_html(".status", + _("that promotion is already over, so updating " + "your bid is kind of pointless, don't you think?")) + # don't create or modify a transaction if no changes have been made. + elif (link.promote_status > promote.STATUS.unpaid and + not address_modified and + getattr(link, "promote_bid", "") == bid): + form.set_html(".status", + _("no changes needed to be made")) + elif pay_id: + # valid bid and created or existing bid id. + # check if already a transaction + if promote.auth_paid_promo(link, c.user, pay_id, bid): + form.redirect(promote.promo_edit_url(link)) + else: + form.set_html(".status", + _("failed to authenticate card. sorry.")) + + @validate(VSponsor("link"), + article = VLink("link")) + def GET_pay(self, article): + data = get_account_info(c.user) + # no need for admins to play in the credit card area + if c.user_is_loggedin and c.user._id != article.author_id: + return self.abort404() + + content = PaymentForm(link = article, + customer_id = data.customerProfileId, + profiles = data.paymentProfiles) + res = LinkInfoPage(link = article, + content = content) + return res.render() + + def GET_link_thumb(self, *a, **kw): + """ + See GET_upload_sr_image for rationale + """ + return "nothing to see here." + + @validate(VSponsor("link_id"), + link = VByName('link_id'), + file = VLength('file', 500*1024)) + def POST_link_thumb(self, link=None, file=None): + errors = dict(BAD_CSS_NAME = "", IMAGE_ERROR = "") + try: + force_thumbnail(link, file) + except cssfilter.BadImage: + # if the image doesn't clean up nicely, abort + errors["IMAGE_ERROR"] = _("bad image") + + if any(errors.values()): + return UploadedImage("", "", "upload", errors = errors).render() + else: + if not c.user_is_sponsor: + promote.unapproved_promo(link) + return UploadedImage(_('saved'), thumbnail_url(link), "", + errors = errors).render() + + diff --git a/r2/r2/controllers/reddit_base.py b/r2/r2/controllers/reddit_base.py index 065832aed..8a961b7e8 100644 --- a/r2/r2/controllers/reddit_base.py +++ b/r2/r2/controllers/reddit_base.py @@ -42,6 +42,7 @@ from Cookie import CookieError from datetime import datetime import sha, simplejson, locale from urllib import quote, unquote +from simplejson import dumps from r2.lib.tracking import encrypt, decrypt @@ -409,10 +410,16 @@ def base_listing(fn): @validate(num = VLimit('limit'), after = VByName('after'), before = VByName('before'), - count = VCount('count')) + count = VCount('count'), + target = VTarget("target")) def new_fn(self, before, **env): + if c.render_style == "htmllite": + c.link_target = env.get("target") + elif "target" in env: + del env["target"] + kw = build_arg_list(fn, env) - + #turn before into after/reverse kw['reverse'] = False if before: @@ -454,8 +461,12 @@ class RedditController(BaseController): c.cookies[g.login_cookie] = Cookie(value='') def pre(self): + c.start_time = datetime.now(g.tz) + g.cache.caches = (LocalCache(),) + g.cache.caches[1:] + c.domain_prefix = request.environ.get("reddit-domain-prefix", + g.domain_prefix) #check if user-agent needs a dose of rate-limiting if not c.error_page: ratelimit_agents() @@ -506,6 +517,11 @@ class RedditController(BaseController): c.have_messages = c.user.msgtime c.user_is_admin = maybe_admin and c.user.name in g.admins c.user_is_sponsor = c.user_is_admin or c.user.name in g.sponsors + if not g.disallow_db_writes: + c.user.update_last_visit(c.start_time) + + #TODO: temporary + c.user_is_paid_sponsor = c.user.name.lower() in g.paid_sponsors c.over18 = over18() @@ -544,7 +560,7 @@ class RedditController(BaseController): elif not c.user.pref_show_stylesheets and not c.cname: c.allow_styles = False #if the site has a cname, but we're not using it - elif c.site.domain and not c.cname: + elif c.site.domain and c.site.css_on_cname and not c.cname: c.allow_styles = False #check content cache @@ -608,6 +624,7 @@ class RedditController(BaseController): and request.method == 'GET' and not c.user_is_loggedin and not c.used_cache + and not c.dontcache and response.status_code != 503 and response.content and response.content[0]): g.rendercache.set(self.request_key(), @@ -645,3 +662,11 @@ class RedditController(BaseController): merged = copy(request.get) merged.update(dict) return request.path + utils.query_string(merged) + + def api_wrapper(self, kw): + data = dumps(kw) + if request.method == "GET" and request.GET.get("callback"): + return "%s(%s)" % (websafe_json(request.GET.get("callback")), + websafe_json(data)) + return self.sendstring(data) + diff --git a/r2/r2/controllers/toolbar.py b/r2/r2/controllers/toolbar.py index 4c26f85d8..97ab9a13a 100644 --- a/r2/r2/controllers/toolbar.py +++ b/r2/r2/controllers/toolbar.py @@ -89,6 +89,8 @@ class ToolbarController(RedditController): "/tb/$id36, show a given link with the toolbar" if not link: return self.abort404() + elif link.is_self: + return self.redirect(link.url) res = Frame(title = link.title, url = link.url, @@ -160,7 +162,7 @@ class ToolbarController(RedditController): wrapper = make_wrapper(render_class = StarkComment, target = "_top") - b = TopCommentBuilder(link, CommentSortMenu.operator('top'), + b = TopCommentBuilder(link, CommentSortMenu.operator('confidence'), wrap = wrapper) listing = NestedListing(b, num = 10, # TODO: add config var diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index 93c9ee9ba..0d1e515ac 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -22,7 +22,7 @@ from pylons import c, request, g from pylons.i18n import _ from pylons.controllers.util import abort -from r2.lib import utils, captcha +from r2.lib import utils, captcha, promote from r2.lib.filters import unkeep_space, websafe, _force_unicode from r2.lib.db.operators import asc, desc from r2.lib.template_helpers import add_sr @@ -30,12 +30,36 @@ from r2.lib.jsonresponse import json_respond, JQueryResponse, JsonResponse from r2.lib.jsontemplates import api_type from r2.models import * +from r2.lib.authorize import Address, CreditCard from r2.controllers.errors import errors, UserRequiredException +from r2.controllers.errors import VerifiedUserRequiredException from copy import copy from datetime import datetime, timedelta import re, inspect +import pycountry + +def visible_promo(article): + is_promo = getattr(article, "promoted", None) is not None + is_author = (c.user_is_loggedin and + c.user._id == article.author_id) + # promos are visible only if comments are not disabled and the + # user is either the author or the link is live/previously live. + if is_promo: + return (not article.disable_comments and + (is_author or + article.promote_status >= promote.STATUS.promoted)) + # not a promo, therefore it is visible + return True + +def can_view_link_comments(article): + return (article.subreddit_slow.can_view(c.user) and + visible_promo(article)) + +def can_comment_link(article): + return (article.subreddit_slow.can_comment(c.user) and + visible_promo(article)) class Validator(object): default_param = None @@ -110,6 +134,8 @@ def validate(*simple_vals, **param_vals): return fn(self, *a, **kw) except UserRequiredException: return self.intermediate_redirect('/login') + except VerifiedUserRequiredException: + return self.intermediate_redirect('/verify') return newfn return val @@ -138,7 +164,10 @@ def api_validate(response_function): simple_vals, param_vals, *a, **kw) except UserRequiredException: responder.send_failure(errors.USER_REQUIRED) - return self.response_func(responder.make_response()) + return self.api_wrapper(responder.make_response()) + except VerifiedUserRequiredException: + responder.send_failure(errors.VERIFIED_USER_REQUIRED) + return self.api_wrapper(responder.make_response()) return newfn return val return _api_validate @@ -147,12 +176,12 @@ def api_validate(response_function): @api_validate def noresponse(self, self_method, responder, simple_vals, param_vals, *a, **kw): self_method(self, *a, **kw) - return self.response_func({}) + return self.api_wrapper({}) @api_validate def json_validate(self, self_method, responder, simple_vals, param_vals, *a, **kw): r = self_method(self, *a, **kw) - return self.response_func(r) + return self.api_wrapper(r) @api_validate def validatedForm(self, self_method, responder, simple_vals, param_vals, @@ -175,7 +204,7 @@ def validatedForm(self, self_method, responder, simple_vals, param_vals, if val: return val else: - return self.response_func(responder.make_response()) + return self.api_wrapper(responder.make_response()) @@ -209,22 +238,58 @@ class VRequired(Validator): else: return item -class VLink(Validator): - def __init__(self, param, redirect = True, *a, **kw): +class VThing(Validator): + def __init__(self, param, thingclass, redirect = True, *a, **kw): Validator.__init__(self, param, *a, **kw) + self.thingclass = thingclass self.redirect = redirect - - def run(self, link_id): - if link_id: + + def run(self, thing_id): + if thing_id: try: - aid = int(link_id, 36) - return Link._byID(aid, True) + tid = int(thing_id, 36) + thing = self.thingclass._byID(tid, True) + if thing.__class__ != self.thingclass: + raise TypeError("Expected %s, got %s" % + (self.thingclass, thing.__class__)) + return thing except (NotFound, ValueError): if self.redirect: abort(404, 'page not found') else: return None +class VLink(VThing): + def __init__(self, param, redirect = True, *a, **kw): + VThing.__init__(self, param, Link, redirect=redirect, *a, **kw) + +class VCommentByID(VThing): + def __init__(self, param, redirect = True, *a, **kw): + VThing.__init__(self, param, Comment, redirect=redirect, *a, **kw) + +class VAward(VThing): + def __init__(self, param, redirect = True, *a, **kw): + VThing.__init__(self, param, Award, redirect=redirect, *a, **kw) + +class VAwardByCodename(Validator): + def run(self, codename, required_fullname=None): + if not codename: + return self.set_error(errors.NO_TEXT) + + try: + a = Award._by_codename(codename) + except NotFound: + a = None + + if a and required_fullname and a._fullname != required_fullname: + return self.set_error(errors.INVALID_OPTION) + else: + return a + +class VTrophy(VThing): + def __init__(self, param, redirect = True, *a, **kw): + VThing.__init__(self, param, Trophy, redirect=redirect, *a, **kw) + class VMessage(Validator): def run(self, message_id): if message_id: @@ -425,10 +490,45 @@ class VAdmin(Validator): if not c.user_is_admin: abort(404, "page not found") -class VSponsor(Validator): +class VVerifiedUser(VUser): def run(self): - if not c.user_is_sponsor: - abort(403, 'forbidden') + VUser.run(self) + if not c.user.email_verified: + raise VerifiedUserRequiredException + +class VSponsor(VVerifiedUser): + def user_test(self, thing): + return (thing.author_id == c.user._id) + + def run(self, link_id = None): + VVerifiedUser.run(self) + if c.user_is_sponsor: + return + elif link_id: + try: + if '_' in link_id: + t = Link._by_fullname(link_id, True) + else: + aid = int(link_id, 36) + t = Link._byID(aid, True) + if self.user_test(t): + return + except (NotFound, ValueError): + pass + abort(403, 'forbidden') + +class VTrafficViewer(VSponsor): + def user_test(self, thing): + return (VSponsor.user_test(self, thing) or + promote.is_traffic_viewer(thing, c.user)) + +# TODO: tempoary validator to be replaced with Vuser once we get he +# bugs worked out +class VPaidSponsor(VSponsor): + def run(self, link_id = None): + if c.user_is_paid_sponsor: + return + VSponsor.run(self, link_id) class VSrModerator(Validator): def run(self): @@ -496,8 +596,10 @@ class VSubmitParent(VByName): if isinstance(parent, Message): return parent else: - sr = parent.subreddit_slow - if c.user_is_loggedin and sr.can_comment(c.user): + link = parent + if isinstance(parent, Comment): + link = Link._byID(parent.link_id) + if c.user_is_loggedin and can_comment_link(link): return parent #else abort(403, "forbidden") @@ -614,6 +716,13 @@ class VExistingUname(VRequired): VRequired.__init__(self, item, errors.NO_USER, *a, **kw) def run(self, name): + if name and name.startswith('~') and c.user_is_admin: + try: + user_id = int(name[1:]) + return Account._byID(user_id) + except (NotFound, ValueError): + return self.error(errors.USER_DOESNT_EXIST) + # make sure the name satisfies our user name regexp before # bothering to look it up. name = chkuser(name) @@ -630,31 +739,74 @@ class VUserWithEmail(VExistingUname): if not user or not hasattr(user, 'email') or not user.email: return self.error(errors.NO_EMAIL_FOR_USER) return user - + class VBoolean(Validator): def run(self, val): return val != "off" and bool(val) -class VInt(Validator): - def __init__(self, param, min=None, max=None, *a, **kw): - self.min = min - self.max = max +class VNumber(Validator): + def __init__(self, param, min=None, max=None, coerce = True, + error = errors.BAD_NUMBER, *a, **kw): + self.min = self.cast(min) if min is not None else None + self.max = self.cast(max) if max is not None else None + self.coerce = coerce + self.error = error Validator.__init__(self, param, *a, **kw) + def cast(self, val): + raise NotImplementedError + def run(self, val): if not val: return - try: - val = int(val) + val = self.cast(val) if self.min is not None and val < self.min: - val = self.min + if self.coerce: + val = self.min + else: + raise ValueError, "" elif self.max is not None and val > self.max: - val = self.max + if self.coerce: + val = self.max + else: + raise ValueError, "" return val except ValueError: - self.set_error(errors.BAD_NUMBER) + self.set_error(self.error, msg_params = dict(min=self.min, + max=self.max)) + +class VInt(VNumber): + def cast(self, val): + return int(val) + +class VFloat(VNumber): + def cast(self, val): + return float(val) + +class VBid(VNumber): + def __init__(self, bid, link_id): + self.duration = 1 + VNumber.__init__(self, (bid, link_id), min = g.min_promote_bid, + max = g.max_promote_bid, coerce = False, + error = errors.BAD_BID) + + def cast(self, val): + return float(val)/self.duration + + def run(self, bid, link_id): + if link_id: + try: + link = Thing._by_fullname(link_id, return_dict = False, + data=True) + self.duration = max((link.promote_until - link._date).days, 1) + except NotFound: + pass + if VNumber.run(self, bid): + return float(bid) + + class VCssName(Validator): """ @@ -728,7 +880,7 @@ class VRatelimit(Validator): field = 'ratelimit') else: self.set_error(self.error) - + @classmethod def ratelimit(self, rate_user = False, rate_ip = False, prefix = "rate_", seconds = None): @@ -750,28 +902,33 @@ class VCommentIDs(Validator): comments = Comment._byID(cids, data=True, return_dict = False) return comments -class VCacheKey(Validator): - def __init__(self, cache_prefix, param, *a, **kw): + +class CachedUser(object): + def __init__(self, cache_prefix, user, key): self.cache_prefix = cache_prefix - self.user = None - self.key = None - Validator.__init__(self, param, *a, **kw) + self.user = user + self.key = key def clear(self): if self.key and self.cache_prefix: g.cache.delete(str(self.cache_prefix + "_" + self.key)) - def run(self, key, name): - self.key = key + +class VCacheKey(Validator): + def __init__(self, cache_prefix, param, *a, **kw): + self.cache_prefix = cache_prefix + Validator.__init__(self, param, *a, **kw) + + def run(self, key): + c_user = CachedUser(self.cache_prefix, None, key) if key: - uid = g.cache.get(str(self.cache_prefix + "_" + self.key)) + uid = g.cache.get(str(self.cache_prefix + "_" + key)) if uid: try: - self.user = Account._byID(uid, data = True) + c_user.user = Account._byID(uid, data = True) except NotFound: return - #found everything we need - return self + return c_user self.set_error(errors.EXPIRED) class VOneOf(Validator): @@ -894,3 +1051,154 @@ class ValidDomain(Validator): if url and is_banned_domain(url): self.set_error(errors.BANNED_DOMAIN) + + + + +class VDate(Validator): + """ + Date checker that accepts string inputs in %m/%d/%Y format. + + Optional parameters include 'past' and 'future' which specify how + far (in days) into the past or future the date must be to be + acceptable. + + NOTE: the 'future' param will have precidence during evaluation. + + Error conditions: + * BAD_DATE on mal-formed date strings (strptime parse failure) + * BAD_FUTURE_DATE and BAD_PAST_DATE on respective range errors. + + """ + def __init__(self, param, future=None, past = None, + admin_override = False, + reference_date = lambda : datetime.now(g.tz), + business_days = False): + self.future = future + self.past = past + + # are weekends to be exluded from the interval? + self.business_days = business_days + + # function for generating "now" + self.reference_date = reference_date + + # do we let admins override date range checking? + self.override = admin_override + Validator.__init__(self, param) + + def run(self, date): + now = self.reference_date() + override = c.user_is_sponsor and self.override + try: + date = datetime.strptime(date, "%m/%d/%Y") + if not override: + # can't put in __init__ since we need the date on the fly + future = utils.make_offset_date(now, self.future, + business_days = self.business_days) + past = utils.make_offset_date(now, self.past, future = False, + business_days = self.business_days) + if self.future is not None and date.date() < future.date(): + self.set_error(errors.BAD_FUTURE_DATE, + {"day": future.days}) + elif self.past is not None and date.date() > past.date(): + self.set_error(errors.BAD_PAST_DATE, + {"day": past.days}) + return date.replace(tzinfo=g.tz) + except (ValueError, TypeError): + self.set_error(errors.BAD_DATE) + +class VDateRange(VDate): + """ + Adds range validation to VDate. In addition to satisfying + future/past requirements in VDate, two date fields must be + provided and they must be in order. + + Additional Error conditions: + * BAD_DATE_RANGE if start_date is not less than end_date + """ + def run(self, *a): + try: + start_date, end_date = [VDate.run(self, x) for x in a] + if not start_date or not end_date or end_date < start_date: + self.set_error(errors.BAD_DATE_RANGE) + return (start_date, end_date) + except ValueError: + # insufficient number of arguments provided (expect 2) + self.set_error(errors.BAD_DATE_RANGE) + + +class VDestination(Validator): + def __init__(self, param = 'dest', default = "", **kw): + self.default = default + Validator.__init__(self, param, **kw) + + def run(self, dest): + return dest or request.referer or self.default + +class ValidAddress(Validator): + def __init__(self, param, usa_only = True): + self.usa_only = usa_only + Validator.__init__(self, param) + + def set_error(self, msg, field): + Validator.set_error(self, errors.BAD_ADDRESS, + dict(message=msg), field = field) + + def run(self, firstName, lastName, company, address, + city, state, zipCode, country, phoneNumber): + if not firstName: + self.set_error(_("please provide a first name"), "firstName") + elif not lastName: + self.set_error(_("please provide a last name"), "lastName") + elif not address: + self.set_error(_("please provide an address"), "address") + elif not city: + self.set_error(_("please provide your city"), "city") + elif not state: + self.set_error(_("please provide your state"), "state") + elif not zipCode: + self.set_error(_("please provide your zip or post code"), "zip") + elif (not self.usa_only and + (not country or not pycountry.countries.get(alpha2=country))): + self.set_error(_("please pick a country"), "country") + else: + if self.usa_only: + country = 'United States' + else: + country = pycountry.countries.get(alpha2=country).name + return Address(firstName = firstName, + lastName = lastName, + company = company or "", + address = address, + city = city, state = state, + zip = zipCode, country = country, + phoneNumber = phoneNumber or "") + +class ValidCard(Validator): + valid_ccn = re.compile(r"\d{13,16}") + valid_date = re.compile(r"\d\d\d\d-\d\d") + valid_ccv = re.compile(r"\d{3,4}") + def set_error(self, msg, field): + Validator.set_error(self, errors.BAD_CARD, + dict(message=msg), field = field) + + def run(self, cardNumber, expirationDate, cardCode): + if not self.valid_ccn.match(cardNumber or ""): + self.set_error(_("credit card numbers should be 13 to 16 digits"), + "cardNumber") + elif not self.valid_date.match(expirationDate or ""): + self.set_error(_("dates should be YYYY-MM"), "expirationDate") + elif not self.valid_ccv.match(cardCode or ""): + self.set_error(_("card verification codes should be 3 or 4 digits"), + "cardCode") + else: + return CreditCard(cardNumber = cardNumber, + expirationDate = expirationDate, + cardCode = cardCode) + +class VTarget(Validator): + target_re = re.compile("^[\w_-]{3,20}$") + def run(self, name): + if name and self.target_re.match(name): + return name diff --git a/r2/r2/i18n/r2.pot b/r2/r2/i18n/r2.pot index 893731c97..2b5621c7c 100644 --- a/r2/r2/i18n/r2.pot +++ b/r2/r2/i18n/r2.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: r2 0.0.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2009-06-09 10:14-0700\n" +"POT-Creation-Date: 2009-08-20 11:28-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,95 +17,108 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.4\n" -#: r2/controllers/api.py:88 r2/controllers/listingcontroller.py:361 +#: r2/controllers/api.py:75 r2/controllers/listingcontroller.py:357 msgid "API" msgstr "" -#: r2/controllers/api.py:111 r2/controllers/feedback.py:35 +#: r2/controllers/api.py:94 msgid "thanks for your message! you should hear back from us shortly." msgstr "" -#: r2/controllers/api.py:140 r2/templates/messagecompose.html:32 +#: r2/controllers/api.py:119 msgid "your message has been delivered" msgstr "" -#: r2/controllers/api.py:403 +#: r2/controllers/api.py:260 +msgid "no title found" +msgstr "" + +#: r2/controllers/api.py:423 msgid "added" msgstr "" -#: r2/controllers/api.py:443 +#: r2/controllers/api.py:474 +msgid "you should be getting a verification email shortly." +msgstr "" + +#: r2/controllers/api.py:476 msgid "your email has been updated" msgstr "" -#: r2/controllers/api.py:453 +#: r2/controllers/api.py:485 msgid "your email and password have been updated" msgstr "" -#: r2/controllers/api.py:456 +#: r2/controllers/api.py:488 msgid "your password has been updated" msgstr "" -#: r2/controllers/api.py:475 +#: r2/controllers/api.py:508 msgid "see? you don't really want to leave" msgstr "" -#: r2/controllers/api.py:603 +#: r2/controllers/api.py:646 msgid "replied" msgstr "" -#: r2/controllers/api.py:669 +#: r2/controllers/api.py:709 msgid "shared" msgstr "" -#: r2/controllers/api.py:672 +#: r2/controllers/api.py:712 msgid "your link has been shared." msgstr "" -#: r2/controllers/api.py:745 +#: r2/controllers/api.py:787 msgid "validation errors" msgstr "" -#: r2/controllers/api.py:766 r2/controllers/api.py:902 r2/controllers/api.py:989 -#: r2/controllers/api.py:1360 r2/controllers/listingcontroller.py:258 -#: r2/lib/menus.py:49 r2/templates/frametoolbar.html:198 r2/templates/link.html:159 +#: r2/controllers/api.py:808 r2/controllers/api.py:944 r2/controllers/api.py:1028 +#: r2/controllers/listingcontroller.py:251 r2/controllers/promotecontroller.py:311 +#: r2/lib/menus.py:49 r2/templates/frametoolbar.html:199 +#: r2/templates/printablebuttons.html:122 msgid "saved" msgstr "" -#: r2/controllers/api.py:832 r2/lib/menus.py:145 r2/templates/printable.html:128 -#: r2/templates/subredditstylesheet.html:236 +#: r2/controllers/api.py:874 r2/lib/menus.py:148 +#: r2/templates/printablebuttons.html:36 r2/templates/subredditstylesheet.html:230 msgid "deleted" msgstr "" -#: r2/controllers/api.py:878 r2/templates/subredditstylesheet.html:195 +#: r2/controllers/api.py:920 r2/templates/subredditstylesheet.html:189 msgid "bad image name" msgstr "" -#: r2/controllers/api.py:885 r2/controllers/api.py:1355 +#: r2/controllers/api.py:927 r2/controllers/promotecontroller.py:304 msgid "bad image" msgstr "" -#: r2/controllers/api.py:889 +#: r2/controllers/api.py:931 #, python-format msgid "too many images (you only get %d)" msgstr "" -#: r2/controllers/api.py:1147 +#: r2/controllers/api.py:1196 msgid "an email will be sent to that account's address shortly" msgstr "" -#: r2/controllers/buttons.py:183 +#: r2/controllers/api.py:1380 +msgid "redirecting..." +msgstr "" + +#: r2/controllers/api.py:1383 +msgid "error (sorry)" +msgstr "" + +#: r2/controllers/buttons.py:176 msgid "reddit buttons" msgstr "" -#: r2/controllers/buttons.py:189 +#: r2/controllers/buttons.py:182 msgid "reddit widget" msgstr "" -#: r2/controllers/buttons.py:194 -msgid "socialite toolbar" -msgstr "" - -#: r2/controllers/buttons.py:200 r2/lib/menus.py:93 +#: r2/controllers/buttons.py:187 r2/lib/menus.py:93 msgid "bookmarklets" msgstr "" @@ -117,9 +130,8 @@ msgstr "" msgid "read this first" msgstr "" -#: r2/controllers/embed.py:62 r2/lib/menus.py:87 r2/lib/pages/pages.py:213 -#: r2/templates/commentreplybox.html:58 r2/templates/frametoolbar.html:102 -#: r2/templates/frametoolbar.html:109 +#: r2/controllers/embed.py:62 r2/lib/menus.py:87 r2/lib/pages/pages.py:281 +#: r2/templates/frametoolbar.html:103 r2/templates/frametoolbar.html:110 msgid "help" msgstr "" @@ -137,302 +149,334 @@ msgid "please login to do that" msgstr "" #: r2/controllers/errors.py:28 -msgid "url required" +msgid "you need to set a valid email address to do that." msgstr "" #: r2/controllers/errors.py:29 -msgid "you should check that url" +msgid "a url is required" msgstr "" #: r2/controllers/errors.py:30 -msgid "title required" +msgid "you should check that url" msgstr "" -#: r2/controllers/errors.py:31 r2/controllers/errors.py:32 -msgid "you can be more succinct than that" +#: r2/controllers/errors.py:31 +msgid "care to try these again?" msgstr "" -#: r2/controllers/errors.py:33 -msgid "your letters stink" -msgstr "" - -#: r2/controllers/errors.py:34 +#: r2/controllers/errors.py:32 msgid "invalid user name" msgstr "" -#: r2/controllers/errors.py:35 +#: r2/controllers/errors.py:33 msgid "that username is already taken" msgstr "" -#: r2/controllers/errors.py:36 +#: r2/controllers/errors.py:34 msgid "id not specified" msgstr "" -#: r2/controllers/errors.py:37 +#: r2/controllers/errors.py:35 msgid "you can't do that" msgstr "" -#: r2/controllers/errors.py:38 -msgid "please enter a comment" -msgstr "" - -#: r2/controllers/errors.py:39 +#: r2/controllers/errors.py:36 msgid "that comment has been deleted" msgstr "" -#: r2/controllers/errors.py:40 +#: r2/controllers/errors.py:37 msgid "that element has been deleted." msgstr "" -#: r2/controllers/errors.py:41 r2/controllers/errors.py:42 +#: r2/controllers/errors.py:38 r2/controllers/errors.py:39 msgid "invalid password" msgstr "" -#: r2/controllers/errors.py:43 +#: r2/controllers/errors.py:40 msgid "passwords do not match" msgstr "" -#: r2/controllers/errors.py:44 +#: r2/controllers/errors.py:41 msgid "please enter a name" msgstr "" -#: r2/controllers/errors.py:45 +#: r2/controllers/errors.py:42 msgid "please enter an email address" msgstr "" -#: r2/controllers/errors.py:46 +#: r2/controllers/errors.py:43 msgid "no email address for that user" msgstr "" -#: r2/controllers/errors.py:47 r2/controllers/errors.py:49 -msgid "please enter a message" -msgstr "" - -#: r2/controllers/errors.py:48 +#: r2/controllers/errors.py:44 msgid "send it to whom?" msgstr "" -#: r2/controllers/errors.py:50 +#: r2/controllers/errors.py:45 msgid "please enter a subject" msgstr "" -#: r2/controllers/errors.py:51 +#: r2/controllers/errors.py:46 msgid "that user doesn't exist" msgstr "" -#: r2/controllers/errors.py:52 +#: r2/controllers/errors.py:47 msgid "please enter a username" msgstr "" -#: r2/controllers/errors.py:54 -msgid "that number isn't in the right range" +#: r2/controllers/errors.py:49 +#, python-format +msgid "that number isn't in the right range (%(min)d to %(max)d)" msgstr "" -#: r2/controllers/errors.py:55 +#: r2/controllers/errors.py:50 msgid "that link has already been submitted" msgstr "" -#: r2/controllers/errors.py:56 +#: r2/controllers/errors.py:51 msgid "that reddit already exists" msgstr "" -#: r2/controllers/errors.py:57 +#: r2/controllers/errors.py:52 msgid "that reddit doesn't exist" msgstr "" -#: r2/controllers/errors.py:58 +#: r2/controllers/errors.py:53 +msgid "you aren't allowed to post there." +msgstr "" + +#: r2/controllers/errors.py:54 +msgid "you must specify a reddit" +msgstr "" + +#: r2/controllers/errors.py:55 msgid "that name isn't going to work" msgstr "" -#: r2/controllers/errors.py:59 +#: r2/controllers/errors.py:56 #, python-format msgid "you are trying to submit too fast. try again in %(time)s." msgstr "" -#: r2/controllers/errors.py:60 +#: r2/controllers/errors.py:57 msgid "your session has expired" msgstr "" -#: r2/controllers/errors.py:61 +#: r2/controllers/errors.py:58 msgid "you must accept the terms first" msgstr "" -#: r2/controllers/errors.py:66 +#: r2/controllers/errors.py:63 msgid "that option is not valid" msgstr "" -#: r2/controllers/errors.py:67 -msgid "description is too long" -msgstr "" - -#: r2/controllers/errors.py:69 +#: r2/controllers/errors.py:65 #, python-format msgid "the following emails are invalid: %(emails)s" msgstr "" -#: r2/controllers/errors.py:70 +#: r2/controllers/errors.py:66 msgid "please enter at least one email address" msgstr "" -#: r2/controllers/errors.py:71 +#: r2/controllers/errors.py:67 #, python-format msgid "please only share to %(num)s emails at a time." msgstr "" -#: r2/controllers/feedback.py:31 +#: r2/controllers/errors.py:68 +msgid "please provide a date of the form mm/dd/yyyy" +msgstr "" + +#: r2/controllers/errors.py:69 +msgid "the dates need to be in order and not identical" +msgstr "" + +#: r2/controllers/errors.py:70 +#, python-format +msgid "please enter a date at least %(day)s days in the future" +msgstr "" + +#: r2/controllers/errors.py:71 +#, python-format +msgid "please enter a date at least %(day)s days in the past" +msgstr "" + +#: r2/controllers/errors.py:72 +#, python-format +msgid "address problem: %(message)s" +msgstr "" + +#: r2/controllers/errors.py:73 +#, python-format +msgid "card problem: %(message)s" +msgstr "" + +#: r2/controllers/errors.py:74 +#, python-format +msgid "this is too long (max: %(max_length)s)" +msgstr "" + +#: r2/controllers/errors.py:75 +msgid "we need something here" +msgstr "" + +#: r2/controllers/feedback.py:30 msgid "inquire about advertising on reddit" msgstr "" -#: r2/controllers/feedback.py:38 -msgid "advertise" +#: r2/controllers/feedback.py:37 +msgid "send reddit feedback" msgstr "" -#: r2/controllers/feedback.py:39 r2/lib/menus.py:92 -msgid "feedback" -msgstr "" - -#: r2/controllers/front.py:100 r2/templates/login.html:87 +#: r2/controllers/front.py:103 r2/templates/login.html:87 msgid "password" msgstr "" -#: r2/controllers/front.py:114 +#: r2/controllers/front.py:117 r2/controllers/front.py:133 +msgid "verify email" +msgstr "" + +#: r2/controllers/front.py:162 msgid "reset password" msgstr "" -#: r2/controllers/front.py:219 r2/controllers/front.py:221 -#: r2/templates/createsubreddit.html:63 +#: r2/controllers/front.py:246 r2/controllers/listingcontroller.py:609 +msgid "comments" +msgstr "" + +#: r2/controllers/front.py:276 r2/controllers/front.py:278 +#: r2/templates/createsubreddit.html:70 msgid "create a reddit" msgstr "" -#: r2/controllers/front.py:250 +#: r2/controllers/front.py:307 msgid "your reddit has been created" msgstr "" -#: r2/controllers/front.py:308 r2/lib/menus.py:85 +#: r2/controllers/front.py:346 r2/lib/menus.py:85 msgid "stats" msgstr "" -#: r2/controllers/front.py:349 r2/controllers/front.py:396 +#: r2/controllers/front.py:376 r2/lib/menus.py:118 +msgid "related" +msgstr "" + +#: r2/controllers/front.py:394 +msgid "other discussions" +msgstr "" + +#: r2/controllers/front.py:411 r2/controllers/front.py:458 msgid "search results" msgstr "" -#: r2/controllers/front.py:495 +#: r2/controllers/front.py:555 msgid "seen it" msgstr "" -#: r2/controllers/front.py:503 r2/lib/menus.py:86 r2/templates/newlink.html:30 -#: r2/templates/newlink.html:89 +#: r2/controllers/front.py:565 r2/lib/menus.py:86 msgid "submit" msgstr "" -#: r2/controllers/front.py:516 r2/controllers/post.py:146 +#: r2/controllers/front.py:578 r2/controllers/post.py:147 msgid "opt out" msgstr "" -#: r2/controllers/front.py:516 r2/controllers/post.py:156 +#: r2/controllers/front.py:578 r2/controllers/post.py:157 msgid "welcome back" msgstr "" -#: r2/controllers/listingcontroller.py:269 r2/controllers/listingcontroller.py:334 +#: r2/controllers/listingcontroller.py:262 r2/controllers/listingcontroller.py:330 msgid "top scoring links" msgstr "" -#: r2/controllers/listingcontroller.py:279 +#: r2/controllers/listingcontroller.py:272 msgid "newest submissions" msgstr "" -#: r2/controllers/listingcontroller.py:336 +#: r2/controllers/listingcontroller.py:332 msgid "most controversial links" msgstr "" -#: r2/controllers/listingcontroller.py:343 +#: r2/controllers/listingcontroller.py:339 msgid "you're really bored now, eh?" msgstr "" -#: r2/controllers/listingcontroller.py:377 +#: r2/controllers/listingcontroller.py:373 msgid "recommended for you" msgstr "" -#: r2/controllers/listingcontroller.py:398 +#: r2/controllers/listingcontroller.py:393 #, python-format msgid "overview for %(user)s" msgstr "" -#: r2/controllers/listingcontroller.py:399 +#: r2/controllers/listingcontroller.py:394 #, python-format msgid "comments by %(user)s" msgstr "" -#: r2/controllers/listingcontroller.py:400 +#: r2/controllers/listingcontroller.py:395 #, python-format msgid "submitted by %(user)s" msgstr "" -#: r2/controllers/listingcontroller.py:401 +#: r2/controllers/listingcontroller.py:396 #, python-format msgid "liked by %(user)s" msgstr "" -#: r2/controllers/listingcontroller.py:402 +#: r2/controllers/listingcontroller.py:397 #, python-format msgid "disliked by %(user)s" msgstr "" -#: r2/controllers/listingcontroller.py:403 +#: r2/controllers/listingcontroller.py:398 #, python-format msgid "hidden by %(user)s" msgstr "" -#: r2/controllers/listingcontroller.py:404 +#: r2/controllers/listingcontroller.py:399 #, python-format msgid "profile for %(user)s" msgstr "" -#: r2/controllers/listingcontroller.py:475 r2/templates/redditheader.html:81 +#: r2/controllers/listingcontroller.py:483 r2/templates/redditheader.html:78 msgid "messages" msgstr "" -#: r2/controllers/listingcontroller.py:485 +#: r2/controllers/listingcontroller.py:493 msgid "comment reply" msgstr "" -#: r2/controllers/listingcontroller.py:531 r2/templates/pagenamenav.html:33 +#: r2/controllers/listingcontroller.py:539 r2/templates/pagenamenav.html:33 msgid "reddits" msgstr "" -#: r2/controllers/listingcontroller.py:565 +#: r2/controllers/listingcontroller.py:573 msgid "reddits: " msgstr "" -#: r2/controllers/listingcontroller.py:601 -msgid "comments" -msgstr "" - -#: r2/controllers/post.py:120 +#: r2/controllers/post.py:121 msgid "over 18?" msgstr "" -#: r2/lib/cssfilter.py:252 +#: r2/controllers/promotecontroller.py:46 +msgid "promoted by you" +msgstr "" + +#: r2/controllers/promotecontroller.py:231 +msgid "that promoted link is already finished." +msgstr "" + +#: r2/controllers/promotecontroller.py:239 +msgid "too late to change the date." +msgstr "" + +#: r2/lib/cssfilter.py:253 msgid "if you need backslashes, you're doing it wrong" msgstr "" -#: r2/lib/emailer.py:127 -#, python-format -msgid "[reddit] %(user)s has shared a link with you" -msgstr "" - -#: r2/lib/emailer.py:130 -msgid "[reddit] a user has shared a link with you" -msgstr "" - -#: r2/lib/emailer.py:135 -msgid "[reddit] email removal notice" -msgstr "" - -#: r2/lib/emailer.py:141 -msgid "[reddit] email addition notice" -msgstr "" - #: r2/lib/menus.py:45 msgid "what's hot" msgstr "" @@ -453,7 +497,7 @@ msgstr "" msgid "recommended" msgstr "" -#: r2/lib/menus.py:51 r2/lib/menus.py:148 +#: r2/lib/menus.py:51 r2/lib/menus.py:151 r2/templates/printablebuttons.html:176 msgid "promote" msgstr "" @@ -481,7 +525,7 @@ msgstr "" msgid "top" msgstr "" -#: r2/lib/menus.py:61 r2/templates/subreddittopbar.html:28 +#: r2/lib/menus.py:61 msgid "more" msgstr "" @@ -489,7 +533,7 @@ msgstr "" msgid "relevance" msgstr "" -#: r2/lib/menus.py:63 r2/templates/buttontypes.html:177 +#: r2/lib/menus.py:63 msgid "controversial" msgstr "" @@ -547,7 +591,7 @@ msgstr "" msgid "turn admin off" msgstr "" -#: r2/lib/menus.py:84 r2/lib/pages/pages.py:389 r2/lib/pages/pages.py:397 +#: r2/lib/menus.py:84 r2/lib/pages/pages.py:429 r2/lib/pages/pages.py:438 msgid "preferences" msgstr "" @@ -555,10 +599,14 @@ msgstr "" msgid "the reddit blog" msgstr "" -#: r2/lib/menus.py:89 r2/templates/utils.html:392 +#: r2/lib/menus.py:89 r2/templates/utils.html:388 msgid "logout" msgstr "" +#: r2/lib/menus.py:92 +msgid "feedback" +msgstr "" + #: r2/lib/menus.py:94 msgid "socialite firefox extension" msgstr "" @@ -611,8 +659,8 @@ msgstr "" msgid "password/email" msgstr "" -#: r2/lib/menus.py:109 r2/templates/prefdelete.html:46 -#: r2/templates/printable.html:128 +#: r2/lib/menus.py:109 r2/templates/prefdelete.html:54 +#: r2/templates/printablebuttons.html:36 msgid "delete" msgstr "" @@ -629,126 +677,155 @@ msgid "sent" msgstr "" #: r2/lib/menus.py:117 -msgid "related" -msgstr "" - -#: r2/lib/menus.py:118 -msgid "details" +msgid "comments {toolbar}" msgstr "" #: r2/lib/menus.py:119 +msgid "details" +msgstr "" + +#: r2/lib/menus.py:120 +#, python-format +msgid "other discussions (%(num)s)" +msgstr "" + +#: r2/lib/menus.py:121 +msgid "shirt" +msgstr "" + +#: r2/lib/menus.py:122 r2/templates/printablebuttons.html:142 msgid "traffic" msgstr "" -#: r2/lib/menus.py:122 +#: r2/lib/menus.py:125 msgid "home" msgstr "" -#: r2/lib/menus.py:123 +#: r2/lib/menus.py:126 msgid "about" msgstr "" -#: r2/lib/menus.py:124 r2/templates/comment.html:146 -#: r2/templates/commentreplybox.html:49 r2/templates/promotedlink.html:35 +#: r2/lib/menus.py:127 r2/templates/printablebuttons.html:108 +#: r2/templates/printablebuttons.html:139 r2/templates/printablebuttons.html:204 msgid "edit" msgstr "" -#: r2/lib/menus.py:125 r2/templates/printable.html:40 -#: r2/templates/printable.html:107 r2/templates/subredditinfobar.html:75 +#: r2/lib/menus.py:128 r2/templates/printable.html:42 +#: r2/templates/printablebuttons.html:48 r2/templates/subredditinfobar.html:75 msgid "banned" msgstr "" -#: r2/lib/menus.py:126 r2/lib/pages/pages.py:1247 +#: r2/lib/menus.py:129 r2/lib/pages/pages.py:1406 msgid "ban users" msgstr "" -#: r2/lib/menus.py:128 +#: r2/lib/menus.py:131 msgid "popular" msgstr "" -#: r2/lib/menus.py:129 r2/templates/admintranslations.html:33 -#: r2/templates/createsubreddit.html:261 r2/templates/promotelinkform.html:144 +#: r2/lib/menus.py:132 r2/templates/admintranslations.html:33 +#: r2/templates/createsubreddit.html:236 msgid "create" msgstr "" -#: r2/lib/menus.py:130 r2/lib/pages/pages.py:800 +#: r2/lib/menus.py:133 r2/lib/pages/pages.py:880 msgid "my reddits" msgstr "" -#: r2/lib/menus.py:132 +#: r2/lib/menus.py:135 msgid "translate site" msgstr "" -#: r2/lib/menus.py:133 +#: r2/lib/menus.py:136 msgid "promoted" msgstr "" -#: r2/lib/menus.py:134 r2admin/controllers/admin.py:77 +#: r2/lib/menus.py:137 msgid "reporters" msgstr "" -#: r2/lib/menus.py:135 r2admin/controllers/admin.py:98 +#: r2/lib/menus.py:138 r2admin/controllers/admin.py:67 msgid "reports" msgstr "" -#: r2/lib/menus.py:136 +#: r2/lib/menus.py:139 msgid "reported authors" msgstr "" -#: r2/lib/menus.py:137 +#: r2/lib/menus.py:140 msgid "info" msgstr "" -#: r2/lib/menus.py:138 r2/templates/link.html:149 r2/templates/sharelink.html:96 +#: r2/lib/menus.py:141 r2/templates/printablebuttons.html:112 +#: r2/templates/sharelink.html:94 msgid "share" msgstr "" -#: r2/lib/menus.py:140 +#: r2/lib/menus.py:143 msgid "overview" msgstr "" -#: r2/lib/menus.py:141 +#: r2/lib/menus.py:144 msgid "submitted" msgstr "" -#: r2/lib/menus.py:142 +#: r2/lib/menus.py:145 msgid "liked" msgstr "" -#: r2/lib/menus.py:143 +#: r2/lib/menus.py:146 msgid "disliked" msgstr "" -#: r2/lib/menus.py:144 +#: r2/lib/menus.py:147 msgid "hidden {toolbar}" msgstr "" -#: r2/lib/menus.py:146 r2/templates/printable.html:123 -#: r2admin/controllers/admin.py:81 +#: r2/lib/menus.py:149 r2/templates/printablebuttons.html:31 msgid "reported" msgstr "" -#: r2/lib/menus.py:149 -msgid "new promoted link" +#: r2/lib/menus.py:152 +msgid "create promotion" msgstr "" -#: r2/lib/menus.py:150 -msgid "promoted links" +#: r2/lib/menus.py:153 +msgid "my promoted links" msgstr "" -#: r2/lib/menus.py:394 -msgid "sort by" +#: r2/lib/menus.py:154 +msgid "all promoted links" msgstr "" -#: r2/lib/menus.py:455 +#: r2/lib/menus.py:155 +msgid "unapproved" +msgstr "" + +#: r2/lib/menus.py:156 +msgid "promo graph" +msgstr "" + +#: r2/lib/menus.py:157 +msgid "live" +msgstr "" + +#: r2/lib/menus.py:158 +msgid "pending" +msgstr "" + +#: r2/lib/menus.py:381 +msgid "sorted by" +msgstr "" + +#: r2/lib/menus.py:442 msgid "kind" msgstr "" -#: r2/lib/menus.py:460 r2/lib/menus.py:509 +#: r2/lib/menus.py:447 r2/lib/menus.py:497 msgid "all" msgstr "" -#: r2/lib/menus.py:470 +#: r2/lib/menus.py:457 msgid "links from" msgstr "" @@ -801,7 +878,7 @@ msgstr "" msgid "your account has been deleted, but we won't judge you for it." msgstr "" -#: r2/lib/strings.py:71 r2/templates/frametoolbar.html:150 +#: r2/lib/strings.py:71 r2/templates/frametoolbar.html:151 msgid "you'll need to login or register to do that" msgstr "" @@ -954,245 +1031,344 @@ msgid "" " " msgstr "" -#: r2/lib/strings.py:183 r2/lib/strings.py:188 r2/lib/pages/pages.py:979 -#: r2/templates/link.html:140 r2/templates/link.htmllite:38 -#: r2/templates/link.htmllite:41 r2/templates/link.xml:29 -#: r2/templates/subredditstylesheet.html:270 +#: r2/lib/strings.py:125 +msgid "" +"You are submitting a link. The key to a successful submission is interesting " +"content and a descriptive title." +msgstr "" + +#: r2/lib/strings.py:126 +msgid "" +"You are submitting a text-based post. Speak your mind. A title is required, " +"but expanding further in the text field is not. Beginning your title with " +"\"vote up if\" is violation of intergalactic law." +msgstr "" + +#: r2/lib/strings.py:127 +msgid "" +"You should consider using [reddit's free iphone " +"app](http://itunes.com/apps/iredditfree)." +msgstr "" + +#: r2/lib/strings.py:128 +msgid "we're going to need to verify your email address for you to proceed." +msgstr "" + +#: r2/lib/strings.py:129 +msgid "your email address has been verfied" +msgstr "" + +#: r2/lib/strings.py:130 +msgid "Verification failed. Please try that again" +msgstr "" + +#: r2/lib/strings.py:190 r2/lib/strings.py:195 r2/lib/template_helpers.py:140 +#: r2/templates/subredditstylesheet.html:264 msgid "comment" msgid_plural "comments" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:184 r2/templates/comment.htmllite:50 -#: r2/templates/linkinfobar.html:32 +#: r2/lib/strings.py:191 r2/templates/comment.htmllite:54 +#: r2/templates/linkinfobar.html:33 msgid "point" msgid_plural "points" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:187 +#: r2/lib/strings.py:194 msgid "link" msgid_plural "links" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:189 r2/lib/pages/pages.py:430 r2/templates/feedback.html:94 -#: r2/templates/messagecompose.html:54 +#: r2/lib/strings.py:196 r2/lib/pages/pages.py:483 msgid "message" msgid_plural "messages" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:190 r2/templates/newlink.html:65 +#: r2/lib/strings.py:197 msgid "subreddit" msgid_plural "subreddits" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:193 r2/templates/subredditinfobar.html:43 +#: r2/lib/strings.py:200 r2/templates/subredditinfobar.html:43 msgid "subscriber" msgid_plural "subscribers" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:194 r2/templates/subreddit.html:80 +#: r2/lib/strings.py:201 r2/templates/subreddit.html:80 #: r2/templates/subreddit.html:81 msgid "contributor" msgid_plural "contributors" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:195 r2/templates/subreddit.html:74 +#: r2/lib/strings.py:202 r2/templates/subreddit.html:74 msgid "moderator" msgid_plural "moderators" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:198 +#: r2/lib/strings.py:205 msgid "milliseconds" msgid_plural "milliseconds" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:199 r2/lib/utils/utils.py:398 r2/templates/searchbar.html:42 +#: r2/lib/strings.py:206 r2/lib/utils/utils.py:412 r2/templates/searchbar.html:42 msgid "second" msgid_plural "seconds" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:200 r2/lib/utils/utils.py:397 +#: r2/lib/strings.py:207 r2/lib/utils/utils.py:411 msgid "minute" msgid_plural "minutes" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:201 r2/lib/utils/utils.py:396 +#: r2/lib/strings.py:208 r2/lib/utils/utils.py:410 msgid "hour" msgid_plural "hours" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:202 r2/lib/utils/utils.py:395 +#: r2/lib/strings.py:209 r2/lib/utils/utils.py:409 msgid "day" msgid_plural "days" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:203 r2/lib/utils/utils.py:394 +#: r2/lib/strings.py:210 r2/lib/utils/utils.py:408 msgid "month" msgid_plural "months" msgstr[0] "" msgstr[1] "" -#: r2/lib/strings.py:204 r2/lib/utils/utils.py:393 +#: r2/lib/strings.py:211 r2/lib/utils/utils.py:407 msgid "year" msgid_plural "years" msgstr[0] "" msgstr[1] "" -#: r2/lib/pages/pages.py:44 +#: r2/lib/template_helpers.py:136 +msgid "comment {verb}" +msgstr "" + +#: r2/lib/pages/pages.py:55 #, python-format msgid "%d %b %Y" msgstr "" -#: r2/lib/pages/pages.py:132 +#: r2/lib/pages/pages.py:148 msgid "Submit a link" msgstr "" -#: r2/lib/pages/pages.py:139 +#: r2/lib/pages/pages.py:155 msgid "Create your own reddit" msgstr "" -#: r2/lib/pages/pages.py:205 +#: r2/lib/pages/pages.py:273 msgid "site links" msgstr "" -#: r2/lib/pages/pages.py:209 +#: r2/lib/pages/pages.py:277 msgid "FAQ" msgstr "" -#: r2/lib/pages/pages.py:211 +#: r2/lib/pages/pages.py:279 msgid "reddiquette" msgstr "" -#: r2/lib/pages/pages.py:222 +#: r2/lib/pages/pages.py:290 msgid "reddit tools" msgstr "" -#: r2/lib/pages/pages.py:226 -msgid "our pet fish" +#: r2/lib/pages/pages.py:297 +msgid "job board" msgstr "" -#: r2/lib/pages/pages.py:231 +#: r2/lib/pages/pages.py:299 msgid "about us" msgstr "" -#: r2/lib/pages/pages.py:247 +#: r2/lib/pages/pages.py:315 msgid "brothers" msgstr "" -#: r2/lib/pages/pages.py:259 +#: r2/lib/pages/pages.py:327 msgid "sisters" msgstr "" -#: r2/lib/pages/pages.py:478 +#: r2/lib/pages/pages.py:531 msgid "login or register" msgstr "" -#: r2/lib/pages/pages.py:547 r2/templates/comment.html:70 -#: r2/templates/comment.html:110 r2/templates/comment.htmllite:42 -#: r2/templates/comment.xml:30 r2/templates/comment.xml:31 +#: r2/lib/pages/pages.py:597 r2/templates/comment.html:74 +#: r2/templates/comment.htmllite:46 r2/templates/comment.xml:30 +#: r2/templates/comment.xml:31 msgid "[deleted]" msgstr "" -#: r2/lib/pages/pages.py:604 r2/templates/createsubreddit.html:57 +#: r2/lib/pages/pages.py:682 r2/templates/createsubreddit.html:60 msgid "manage your reddit" msgstr "" -#: r2/lib/pages/pages.py:605 +#: r2/lib/pages/pages.py:683 #, python-format msgid "about %(site)s" msgstr "" -#: r2/lib/pages/pages.py:632 +#: r2/lib/pages/pages.py:708 msgid "search reddits" msgstr "" -#: r2/lib/pages/pages.py:754 +#: r2/lib/pages/pages.py:830 msgid "page not found" msgstr "" -#: r2/lib/pages/pages.py:765 +#: r2/lib/pages/pages.py:841 msgid "you aren't allowed to do that." msgstr "" -#: r2/lib/pages/pages.py:867 +#: r2/lib/pages/pages.py:979 msgid "try entering those letters again" msgstr "" -#: r2/lib/pages/pages.py:924 +#: r2/lib/pages/pages.py:1029 msgid "previous search" msgstr "" -#: r2/lib/pages/pages.py:944 +#: r2/lib/pages/pages.py:1049 #, python-format msgid "%(site_title)s via %(domain)s" msgstr "" -#: r2/lib/pages/pages.py:976 r2/templates/commentreplybox.html:46 -#: r2/templates/link.html:137 -msgid "comment {verb}" -msgstr "" - -#: r2/lib/pages/pages.py:1131 +#: r2/lib/pages/pages.py:1290 msgid "This feature is currently unavailable. Sorry" msgstr "" -#: r2/lib/pages/pages.py:1198 +#: r2/lib/pages/pages.py:1357 msgid "add a friend" msgstr "" -#: r2/lib/pages/pages.py:1202 +#: r2/lib/pages/pages.py:1361 msgid "your friends" msgstr "" -#: r2/lib/pages/pages.py:1217 +#: r2/lib/pages/pages.py:1376 msgid "add contributor" msgstr "" -#: r2/lib/pages/pages.py:1221 +#: r2/lib/pages/pages.py:1380 #, python-format msgid "contributors to %(reddit)s" msgstr "" -#: r2/lib/pages/pages.py:1232 +#: r2/lib/pages/pages.py:1391 msgid "add moderator" msgstr "" -#: r2/lib/pages/pages.py:1236 +#: r2/lib/pages/pages.py:1395 #, python-format msgid "moderators to %(reddit)s" msgstr "" -#: r2/lib/pages/pages.py:1251 +#: r2/lib/pages/pages.py:1410 msgid "banned users" msgstr "" -#: r2/lib/utils/utils.py:411 +#: r2/lib/utils/utils.py:425 msgid "millisecond" msgid_plural "milliseconds" msgstr[0] "" msgstr[1] "" -#: r2/lib/utils/utils.py:422 r2/templates/comment.htmllite:42 +#: r2/lib/utils/utils.py:436 r2/templates/comment.htmllite:46 msgid "ago" msgstr "" -#: r2/models/subreddit.py:548 +#: r2/models/builder.py:56 +msgid "friend" +msgstr "" + +#: r2/models/builder.py:63 +msgid "submitter" +msgstr "" + +#: r2/models/builder.py:77 +msgid "reddit admin, speaking officially" +msgstr "" + +#: r2/models/builder.py:137 +#, python-format +msgid "moderator of /r/%(reddit)s, speaking officially" +msgstr "" + +#: r2/models/mail_queue.py:303 +#, python-format +msgid "[reddit] %(user)s has shared a link with you" +msgstr "" + +#: r2/models/mail_queue.py:304 +#, python-format +msgid "[feedback] feedback from '%(user)s'" +msgstr "" + +#: r2/models/mail_queue.py:305 +#, python-format +msgid "[ad_inq] feedback from '%(user)s'" +msgstr "" + +#: r2/models/mail_queue.py:306 +msgid "[reddit] email removal notice" +msgstr "" + +#: r2/models/mail_queue.py:307 +msgid "[reddit] email addition notice" +msgstr "" + +#: r2/models/mail_queue.py:308 +msgid "[reddit] reset your password" +msgstr "" + +#: r2/models/mail_queue.py:309 +msgid "[reddit] verify your email address" +msgstr "" + +#: r2/models/mail_queue.py:310 +msgid "[reddit] your bid has been accepted" +msgstr "" + +#: r2/models/mail_queue.py:311 +msgid "[reddit] your promotion has been accepted" +msgstr "" + +#: r2/models/mail_queue.py:312 +msgid "[reddit] your promotion has been rejected" +msgstr "" + +#: r2/models/mail_queue.py:313 +msgid "[reddit] your promotion has been queued" +msgstr "" + +#: r2/models/mail_queue.py:314 +msgid "[reddit] your promotion is now live" +msgstr "" + +#: r2/models/mail_queue.py:315 +msgid "[reddit] your promotion has finished" +msgstr "" + +#: r2/models/subreddit.py:568 msgid "reddit.com: what's new online!" msgstr "" -#: r2/models/subreddit.py:592 +#: r2/models/subreddit.py:616 msgid "on reddit.com" msgstr "" @@ -1385,37 +1561,11 @@ msgstr "" msgid "hide code" msgstr "" -#: r2/templates/buttontypes.html:175 -msgid "quite controversial" -msgstr "" - -#: r2/templates/buttontypes.html:179 -msgid "uncontroversial" -msgstr "" - -#: r2/templates/buttontypes.html:181 -msgid "quite uncontroversial" -msgstr "" - -#: r2/templates/buttontypes.html:198 -msgid "Vote on this article" -msgstr "" - -#: r2/templates/buttontypes.html:203 -#, python-format -msgid "Discuss at the %(name)s reddit" -msgstr "" - -#: r2/templates/buttontypes.html:205 -#, python-format -msgid "Submit to the %(name)s reddit" -msgstr "" - -#: r2/templates/captcha.html:54 +#: r2/templates/captcha.html:75 msgid "human?" msgstr "" -#: r2/templates/captcha.html:64 +#: r2/templates/captcha.html:84 msgid "type the letters from the image above" msgstr "" @@ -1427,89 +1577,37 @@ msgstr "" msgid "clear" msgstr "" -#: r2/templates/comment.html:66 +#: r2/templates/comment.html:70 msgid "deleted comment from" msgstr "" -#: r2/templates/comment.html:74 +#: r2/templates/comment.html:78 msgid "comment score below threshold" msgstr "" -#: r2/templates/comment.html:79 +#: r2/templates/comment.html:84 #, python-format msgid "%(timeago)s ago" msgstr "" -#: r2/templates/comment.html:91 +#: r2/templates/comment.html:96 msgid "+" msgstr "" -#: r2/templates/comment.html:91 +#: r2/templates/comment.html:96 msgid "-" msgstr "" -#: r2/templates/comment.html:94 +#: r2/templates/comment.html:99 msgid "child" msgid_plural "children" msgstr[0] "" msgstr[1] "" -#: r2/templates/comment.html:128 -msgid "permalink" -msgstr "" - -#: r2/templates/comment.html:134 r2/templates/comment.html:141 -msgid "parent" -msgstr "" - -#: r2/templates/comment.html:154 r2/templates/message.html:84 -msgid "reply {verb}" -msgstr "" - -#: r2/templates/comment.html:155 r2/templates/commentreplybox.html:52 -#: r2/templates/link.html:149 r2/templates/link.html:181 -#: r2/templates/sharelink.html:100 -msgid "cancel" -msgstr "" - -#: r2/templates/comment.xml:38 +#: r2/templates/comment.xml:39 msgid "on" msgstr "" -#: r2/templates/commentreplybox.html:60 -msgid "hide help" -msgstr "" - -#: r2/templates/commentreplybox.html:65 -msgid "enter a comment here" -msgstr "" - -#: r2/templates/commentreplybox.html:71 -msgid "you type:" -msgstr "" - -#: r2/templates/commentreplybox.html:72 -msgid "you see:" -msgstr "" - -#: r2/templates/commentreplybox.html:73 r2/templates/commentreplybox.html:74 -msgid "italics" -msgstr "" - -#: r2/templates/commentreplybox.html:75 r2/templates/commentreplybox.html:76 -msgid "bold" -msgstr "" - -#: r2/templates/commentreplybox.html:79 r2/templates/commentreplybox.html:80 -#: r2/templates/commentreplybox.html:81 r2/templates/commentreplybox.html:82 -#: r2/templates/commentreplybox.html:83 r2/templates/commentreplybox.html:84 -msgid "item" -msgstr "" - -#: r2/templates/commentreplybox.html:85 r2/templates/commentreplybox.html:86 -msgid "quoted text" -msgstr "" - #: r2/templates/commentspanel.html:37 msgid "This sidebar will automatically appear when there are comments." msgstr "" @@ -1518,304 +1616,144 @@ msgstr "" msgid "This sidebar will not be shown automatically." msgstr "" -#: r2/templates/createsubreddit.html:61 +#: r2/templates/createsubreddit.html:68 msgid "that subreddit doesn't exist, but you can create it here." msgstr "" -#: r2/templates/createsubreddit.html:75 -msgid "name" -msgstr "" - -#: r2/templates/createsubreddit.html:91 -msgid "no spaces, e.g. slashdot" -msgstr "" - -#: r2/templates/createsubreddit.html:98 r2/templates/newlink.html:55 -#: r2/templates/promotelinkform.html:44 -msgid "title" -msgstr "" - -#: r2/templates/createsubreddit.html:113 -msgid "e.g. slashdot: news for nerds, stuff that matters" -msgstr "" - -#: r2/templates/createsubreddit.html:121 -msgid "description" -msgstr "" - -#: r2/templates/createsubreddit.html:137 -msgid "language" -msgstr "" - -#: r2/templates/createsubreddit.html:151 -msgid "type" -msgstr "" - -#: r2/templates/createsubreddit.html:155 -msgid "public" -msgstr "" - -#: r2/templates/createsubreddit.html:155 -msgid "anyone can view and submit" -msgstr "" - -#: r2/templates/createsubreddit.html:156 -msgid "restricted" -msgstr "" - -#: r2/templates/createsubreddit.html:157 -msgid "anyone can view, but only contributors can submit links" -msgstr "" - -#: r2/templates/createsubreddit.html:158 r2/templates/subreddit.html:92 -msgid "private" -msgstr "" - -#: r2/templates/createsubreddit.html:158 -msgid "only contributors can view and submit" -msgstr "" - -#: r2/templates/createsubreddit.html:164 -msgid "age" -msgstr "" - -#: r2/templates/createsubreddit.html:169 -msgid "viewers must be over eighteen years old" -msgstr "" - -#: r2/templates/createsubreddit.html:174 r2/templates/prefoptions.html:105 -msgid "media" -msgstr "" - -#: r2/templates/createsubreddit.html:180 -msgid "show thumbnail images of content" -msgstr "" - -#: r2/templates/createsubreddit.html:186 -msgid "domain" -msgstr "" - -#: r2/templates/createsubreddit.html:196 -msgid "" -"Own a domain? Enter it here and then go to your DNS provider and add a CNAME" -" record aliasing your domain to rhs.reddit.com. You will be able to access " -"your reddit through your domain." -msgstr "" - -#: r2/templates/createsubreddit.html:209 -msgid "look and feel" -msgstr "" - -#: r2/templates/createsubreddit.html:213 -msgid "edit the stylesheet" -msgstr "" - -#: r2/templates/createsubreddit.html:217 -msgid "leaves this page" -msgstr "" - -#: r2/templates/createsubreddit.html:225 -msgid "upload header image" -msgstr "" - -#: r2/templates/createsubreddit.html:232 -msgid "restore default header" -msgstr "" - -#: r2/templates/createsubreddit.html:258 r2/templates/prefoptions.html:187 -#: r2/templates/promotelinkform.html:141 +#: r2/templates/createsubreddit.html:233 r2/templates/prefoptions.html:187 +#: r2/templates/promotelinkform.html:351 msgid "save options" msgstr "" -#: r2/templates/feedback.html:48 r2/templates/sharelink.html:33 -msgid "your name" -msgstr "" - -#: r2/templates/feedback.html:57 r2/templates/login.html:77 -#: r2/templates/prefupdate.html:34 -msgid "email" -msgstr "" - -#: r2/templates/feedback.html:66 -msgid "reply to" -msgstr "" - -#: r2/templates/feedback.html:75 r2/templates/login.html:77 -msgid "optional" -msgstr "" - -#: r2/templates/feedback.html:85 -msgid "put your email here" -msgstr "" - -#: r2/templates/feedback.html:88 -msgid "(only used to reply)" -msgstr "" - -#: r2/templates/feedback.html:106 r2/templates/messagecompose.html:71 +#: r2/templates/feedback.html:70 msgid "send" msgstr "" -#: r2/templates/frametoolbar.html:55 +#: r2/templates/frametoolbar.html:56 msgid "caution: dorkbar detected" msgstr "" -#: r2/templates/frametoolbar.html:68 +#: r2/templates/frametoolbar.html:69 msgid "show url" msgstr "" -#: r2/templates/frametoolbar.html:71 +#: r2/templates/frametoolbar.html:72 msgid "hide url" msgstr "" -#: r2/templates/frametoolbar.html:75 +#: r2/templates/frametoolbar.html:76 msgid "load a random link" msgstr "" -#: r2/templates/frametoolbar.html:82 +#: r2/templates/frametoolbar.html:83 msgid "serendipity" msgstr "" -#: r2/templates/frametoolbar.html:90 +#: r2/templates/frametoolbar.html:91 msgid "visit your userpage" msgstr "" -#: r2/templates/frametoolbar.html:94 +#: r2/templates/frametoolbar.html:95 msgid "login / register" msgstr "" -#: r2/templates/frametoolbar.html:115 r2/templates/frametoolbar.html:116 +#: r2/templates/frametoolbar.html:116 r2/templates/frametoolbar.html:117 msgid "visit this link without the toolbar" msgstr "" -#: r2/templates/frametoolbar.html:123 +#: r2/templates/frametoolbar.html:124 msgid "click to visit the main page for this submission" msgstr "" -#: r2/templates/frametoolbar.html:135 +#: r2/templates/frametoolbar.html:136 msgid "click to submit this link to reddit" msgstr "" -#: r2/templates/frametoolbar.html:172 +#: r2/templates/frametoolbar.html:173 #, python-format msgid "vote %(direction)s" msgstr "" -#: r2/templates/frametoolbar.html:186 +#: r2/templates/frametoolbar.html:187 msgid "like" msgstr "" -#: r2/templates/frametoolbar.html:187 +#: r2/templates/frametoolbar.html:188 msgid "dislike" msgstr "" -#: r2/templates/frametoolbar.html:193 r2/templates/link.html:155 +#: r2/templates/frametoolbar.html:194 r2/templates/printablebuttons.html:118 msgid "unsave" msgstr "" -#: r2/templates/frametoolbar.html:194 r2/templates/link.html:156 +#: r2/templates/frametoolbar.html:195 r2/templates/printablebuttons.html:119 msgid "unsaved" msgstr "" -#: r2/templates/frametoolbar.html:197 r2/templates/link.html:158 -#: r2/templates/prefupdate.html:52 r2/templates/subredditstylesheet.html:79 +#: r2/templates/frametoolbar.html:198 r2/templates/prefupdate.html:88 +#: r2/templates/printablebuttons.html:121 r2/templates/subredditstylesheet.html:79 msgid "save" msgstr "" -#: r2/templates/frametoolbar.html:204 +#: r2/templates/frametoolbar.html:205 msgid "toggle the comments panel" msgstr "" -#: r2/templates/link.html:118 +#: r2/templates/link.html:165 #, python-format msgid "submitted %(when)s ago by %(author)s to %(reddit)s" msgstr "" -#: r2/templates/link.html:120 +#: r2/templates/link.html:167 #, python-format msgid "submitted %(when)s ago by %(author)s" msgstr "" -#: r2/templates/link.html:163 -msgid "unhide" -msgstr "" - -#: r2/templates/link.html:164 -msgid "unhidden" -msgstr "" - -#: r2/templates/link.html:166 -msgid "hide" -msgstr "" - -#: r2/templates/link.html:167 -msgid "hidden" -msgstr "" - -#: r2/templates/link.html:181 -msgid "watch" -msgstr "" - -#: r2/templates/linkcompressed.html:49 -#, python-format -msgid "posted %(when)s ago by %(author)s" -msgstr "" - -#: r2/templates/linkcompressed.html:51 -#, python-format -msgid "%(points)s posted %(when)s ago by %(author)s" -msgstr "" - -#: r2/templates/linkinfobar.html:27 +#: r2/templates/linkinfobar.html:28 msgid "toolbar link" msgstr "" -#: r2/templates/linkinfobar.html:30 +#: r2/templates/linkinfobar.html:31 msgid "submitted on" msgstr "" -#: r2/templates/linkinfobar.html:34 +#: r2/templates/linkinfobar.html:35 msgid "up votes" msgstr "" -#: r2/templates/linkinfobar.html:36 +#: r2/templates/linkinfobar.html:37 msgid "down votes" msgstr "" -#: r2/templates/linkpromoteinfobar.html:30 +#: r2/templates/linkpromoteinfobar.html:31 msgid "promoted on" msgstr "" -#: r2/templates/linkpromoteinfobar.html:37 -msgid "promoted by" +#: r2/templates/linkpromoteinfobar.html:39 +msgid "unpromoted on" msgstr "" -#: r2/templates/linkpromoteinfobar.html:45 +#: r2/templates/linkpromoteinfobar.html:47 msgid "promote until" msgstr "" -#: r2/templates/linkpromoteinfobar.html:49 +#: r2/templates/linkpromoteinfobar.html:51 msgid "(this link has expired and is no longer being promoted)" msgstr "" -#: r2/templates/linkpromoteinfobar.html:57 -#, python-format -msgid "shown only to subscribers of %(subreddit)s" -msgstr "" - -#: r2/templates/listing.html:37 +#: r2/templates/listing.html:36 msgid "view more:" msgstr "" -#: r2/templates/listing.html:39 +#: r2/templates/listing.html:38 msgid "prev" msgstr "" -#: r2/templates/listing.html:45 +#: r2/templates/listing.html:44 msgid "next" msgstr "" -#: r2/templates/listing.html:50 +#: r2/templates/listing.html:49 msgid "there doesn't seem to be anything here" msgstr "" @@ -1823,13 +1761,19 @@ msgstr "" msgid "you are logged in. go use the site!" msgstr "" -#: r2/templates/login.html:66 r2/templates/password.html:35 -#: r2/templates/resetpassword.html:38 +#: r2/templates/login.html:66 msgid "username" msgstr "" -#: r2/templates/login.html:98 r2/templates/prefupdate.html:46 -#: r2/templates/resetpassword.html:59 +#: r2/templates/login.html:77 +msgid "email" +msgstr "" + +#: r2/templates/login.html:77 +msgid "optional" +msgstr "" + +#: r2/templates/login.html:98 msgid "verify password" msgstr "" @@ -1866,38 +1810,30 @@ msgstr "" msgid "recover password" msgstr "" -#: r2/templates/message.html:42 r2/templates/message.xml:32 +#: r2/templates/message.html:43 r2/templates/message.xml:33 #, python-format msgid "from %(author)s sent %(when)s ago" msgstr "" -#: r2/templates/message.html:44 r2/templates/message.xml:34 +#: r2/templates/message.html:45 r2/templates/message.xml:35 #, python-format msgid "to %(dest)s sent %(when)s ago" msgstr "" -#: r2/templates/message.html:46 +#: r2/templates/message.html:47 #, python-format msgid "to %(dest)s from %(author)s sent %(when)s ago" msgstr "" -#: r2/templates/message.html:76 r2/templates/starkcomment.html:33 -msgid "context" -msgstr "" - -#: r2/templates/messagecompose.html:25 +#: r2/templates/messagecompose.html:30 msgid "send a message" msgstr "" -#: r2/templates/messagecompose.html:36 -msgid "to (username)" -msgstr "" - -#: r2/templates/messagecompose.html:44 -msgid "subject" -msgstr "" - #: r2/templates/morechildren.html:36 +msgid "load more comments" +msgstr "" + +#: r2/templates/morechildren.html:37 msgid "reply" msgid_plural "replies" msgstr[0] "" @@ -1908,33 +1844,7 @@ msgid "continue this thread" msgstr "" #: r2/templates/newlink.html:32 -#, python-format -msgid "submit to %(site)s" -msgstr "" - -#: r2/templates/newlink.html:44 r2/templates/promotelinkform.html:53 -msgid "url" -msgstr "" - -#: r2/templates/newlink.html:80 -msgid "add to my saved sites" -msgstr "" - -#: r2/templates/newlink.html:100 -msgid "type \"self\" here to make this post refer to itself." -msgstr "" - -#: r2/templates/newlink.html:102 -msgid "enter a title, or click submit to find one automatically." -msgstr "" - -#: r2/templates/newlink.html:107 -#, python-format -msgid "use reddit from your toolbar with the %(bookmarklets)s" -msgstr "" - -#: r2/templates/newlink.html:108 -msgid "reddit bookmarklets" +msgid "submit to reddit" msgstr "" #: r2/templates/optout.html:26 @@ -1954,12 +1864,12 @@ msgid "Would you like the address %(email)s to no longer receive email from us?" msgstr "" #: r2/templates/optout.html:36 r2/templates/optout.html:52 -#: r2/templates/prefdelete.html:27 r2/templates/printable.html:291 +#: r2/templates/prefdelete.html:28 r2/templates/printablebuttons.html:293 msgid "yes" msgstr "" #: r2/templates/optout.html:39 r2/templates/optout.html:55 -#: r2/templates/prefdelete.html:27 r2/templates/printable.html:294 +#: r2/templates/prefdelete.html:28 r2/templates/printablebuttons.html:296 msgid "no" msgstr "" @@ -1973,35 +1883,35 @@ msgstr "" msgid "Allow '%(email)s' to receive email from us?" msgstr "" -#: r2/templates/organiclisting.html:61 +#: r2/templates/organiclisting.html:60 msgid "what's this?" msgstr "" -#: r2/templates/organiclisting.html:67 +#: r2/templates/organiclisting.html:66 msgid "" "This area shows new and upcoming links. Vote on links here to help them " "become popular, and click the forwards and backwards buttons to view more. " msgstr "" -#: r2/templates/organiclisting.html:72 r2/templates/organiclisting.html:82 +#: r2/templates/organiclisting.html:71 r2/templates/organiclisting.html:81 msgid "here" msgstr "" -#: r2/templates/organiclisting.html:72 +#: r2/templates/organiclisting.html:71 msgid "This element has been disabled." msgstr "" -#: r2/templates/organiclisting.html:74 +#: r2/templates/organiclisting.html:73 #, python-format msgid "Click %(here)s to disable this feature." msgstr "" -#: r2/templates/organiclisting.html:81 +#: r2/templates/organiclisting.html:80 #, python-format msgid "Click %(here)s to close help." msgstr "" -#: r2/templates/organiclisting.html:95 +#: r2/templates/organiclisting.html:94 msgid "" "The new link area will no longer appear for you. To re-enable it, visit your " "preferences." @@ -2015,39 +1925,31 @@ msgstr "" msgid "are you over eighteen and willing to see adult content?" msgstr "" -#: r2/templates/password.html:24 -msgid "you should receive an email shortly" -msgstr "" - -#: r2/templates/password.html:30 +#: r2/templates/password.html:29 msgid "what's my password?" msgstr "" -#: r2/templates/password.html:31 +#: r2/templates/password.html:30 msgid "enter your user name below to receive your login information" msgstr "" -#: r2/templates/password.html:41 +#: r2/templates/password.html:40 msgid "email me" msgstr "" +#: r2/templates/permalinkmessage.html:24 +msgid "you are viewing a single comment's thread." +msgstr "" + #: r2/templates/permalinkmessage.html:26 -msgid "you are viewing a single comment's thread" +msgid "view the rest of the comments" msgstr "" -#: r2/templates/permalinkmessage.html:28 -msgid "see the above comments" -msgstr "" - -#: r2/templates/prefdelete.html:26 r2/templates/printable.html:273 -msgid "are you sure?" -msgstr "" - -#: r2/templates/prefdelete.html:37 +#: r2/templates/prefdelete.html:45 msgid "delete your reddit account? hope you have a good reason." msgstr "" -#: r2/templates/prefdelete.html:40 +#: r2/templates/prefdelete.html:48 msgid "deleting..." msgstr "" @@ -2079,6 +1981,10 @@ msgstr "" msgid "display links with a reddit toolbar" msgstr "" +#: r2/templates/prefoptions.html:105 +msgid "media" +msgstr "" + #: r2/templates/prefoptions.html:108 msgid "show thumbnails next to links" msgstr "" @@ -2191,39 +2097,136 @@ msgstr "" msgid "required to view some reddits" msgstr "" -#: r2/templates/prefupdate.html:29 -msgid "current password (required)" +#: r2/templates/prefupdate.html:28 +msgid "update your email or password" msgstr "" -#: r2/templates/prefupdate.html:41 r2/templates/resetpassword.html:47 -msgid "new password" +#: r2/templates/prefupdate.html:31 +msgid "verify your email" msgstr "" -#: r2/templates/printable.html:101 r2/templates/subredditinfobar.html:68 -msgid "unban" +#: r2/templates/prefupdate.html:33 +msgid "update your email" msgstr "" -#: r2/templates/printable.html:102 r2/templates/subredditinfobar.html:69 -msgid "unbanned" +#: r2/templates/prefupdate.html:36 +msgid "update your password" msgstr "" -#: r2/templates/printable.html:106 r2/templates/subredditinfobar.html:74 -msgid "ban" +#: r2/templates/prefupdate.html:54 +msgid "verified" msgstr "" -#: r2/templates/printable.html:112 -msgid "ignore" +#: r2/templates/prefupdate.html:56 +msgid "verification pending" msgstr "" -#: r2/templates/printable.html:113 -msgid "ignored" +#: r2/templates/prefupdate.html:58 +msgid "unverified" msgstr "" -#: r2/templates/printable.html:123 +#: r2/templates/prefupdate.html:86 +msgid "send verification email" +msgstr "" + +#: r2/templates/printablebuttons.html:31 msgid "report" msgstr "" -#: r2/templates/printable.html:345 +#: r2/templates/printablebuttons.html:42 r2/templates/subredditinfobar.html:68 +msgid "unban" +msgstr "" + +#: r2/templates/printablebuttons.html:43 r2/templates/subredditinfobar.html:69 +msgid "unbanned" +msgstr "" + +#: r2/templates/printablebuttons.html:47 r2/templates/subredditinfobar.html:74 +msgid "ban" +msgstr "" + +#: r2/templates/printablebuttons.html:53 +msgid "ignore" +msgstr "" + +#: r2/templates/printablebuttons.html:54 +msgid "ignored" +msgstr "" + +#: r2/templates/printablebuttons.html:69 +msgid "distinguishing..." +msgstr "" + +#: r2/templates/printablebuttons.html:71 +msgid "distinguish" +msgstr "" + +#: r2/templates/printablebuttons.html:73 +msgid "distinguish this?" +msgstr "" + +#: r2/templates/printablebuttons.html:112 r2/templates/printablebuttons.html:164 +#: r2/templates/sharelink.html:98 +msgid "cancel" +msgstr "" + +#: r2/templates/printablebuttons.html:126 +msgid "unhide" +msgstr "" + +#: r2/templates/printablebuttons.html:127 +msgid "unhidden" +msgstr "" + +#: r2/templates/printablebuttons.html:129 +msgid "hide" +msgstr "" + +#: r2/templates/printablebuttons.html:130 +msgid "hidden" +msgstr "" + +#: r2/templates/printablebuttons.html:146 +msgid "unpromote" +msgstr "" + +#: r2/templates/printablebuttons.html:146 +msgid "unpromoted" +msgstr "" + +#: r2/templates/printablebuttons.html:164 +msgid "reject" +msgstr "" + +#: r2/templates/printablebuttons.html:172 +msgid "accept" +msgstr "" + +#: r2/templates/printablebuttons.html:172 +msgid "accepted" +msgstr "" + +#: r2/templates/printablebuttons.html:187 +msgid "permalink" +msgstr "" + +#: r2/templates/printablebuttons.html:192 r2/templates/printablebuttons.html:199 +msgid "parent" +msgstr "" + +#: r2/templates/printablebuttons.html:212 r2/templates/printablebuttons.html:228 +msgid "reply {verb}" +msgstr "" + +#: r2/templates/printablebuttons.html:222 r2/templates/starkcomment.html:33 +msgid "context" +msgstr "" + +#: r2/templates/printablebuttons.html:275 +msgid "are you sure?" +msgstr "" + +#: r2/templates/printablebuttons.html:360 msgid "toggle" msgstr "" @@ -2240,7 +2243,7 @@ msgstr "" msgid "user for %(time)s" msgstr "" -#: r2/templates/profilebar.html:81 r2/templates/usertableitem.html:43 +#: r2/templates/profilebar.html:81 r2/templates/usertableitem.html:46 msgid "send message" msgstr "" @@ -2252,86 +2255,105 @@ msgstr "" msgid "remove from friends" msgstr "" -#: r2/templates/promotedlink.html:39 r2/templates/promotedlinks.html:43 -msgid "unpromote" -msgstr "" - -#: r2/templates/promotedlink.html:39 r2/templates/promotedlinks.html:43 -msgid "unpromoted" -msgstr "" - -#: r2/templates/promotedlink.html:54 -msgid "sponsored link" -msgstr "" - -#: r2/templates/promotedlinks.html:30 -msgid "current promotions" -msgstr "" - -#: r2/templates/promotedlinks.html:37 -#, python-format -msgid "(until %(until)s)" -msgstr "" - -#: r2/templates/promotedlinks.html:52 +#: r2/templates/promote_graph.html:76 msgid "promotions this month" msgstr "" -#: r2/templates/promotelinkform.html:65 -msgid "reddit" -msgstr "" - -#: r2/templates/promotelinkform.html:69 +#: r2/templates/promotedlink.html:35 #, python-format -msgid "show only to subscribers of %(reddit)s" +msgid "to be promoted by %(author)s" msgstr "" -#: r2/templates/promotelinkform.html:76 -msgid "show only to subscribers of this reddit" -msgstr "" - -#: r2/templates/promotelinkform.html:85 -msgid "site options" -msgstr "" - -#: r2/templates/promotelinkform.html:88 -msgid "disable comments" -msgstr "" - -#: r2/templates/promotelinkform.html:94 -msgid "duration" -msgstr "" - -#: r2/templates/promotelinkform.html:98 +#: r2/templates/promotedlink.html:37 #, python-format -msgid "expire in %(timedelta)s (%(expires_at)s)" +msgid "promoted %(when)s ago by %(author)s" msgstr "" -#: r2/templates/promotelinkform.html:102 r2/templates/promotelinkform.html:115 -msgid "expire in" +#: r2/templates/promotedlink.html:57 +msgid "unverified sponsored link" msgstr "" -#: r2/templates/promotelinkform.html:105 r2/templates/promotelinkform.html:118 -msgid "hours" +#: r2/templates/promotedlink.html:59 +msgid "unapproved sponsored link" msgstr "" -#: r2/templates/promotelinkform.html:106 r2/templates/promotelinkform.html:119 -msgid "days" +#: r2/templates/promotedlink.html:61 +msgid "pending sponsored link" msgstr "" -#: r2/templates/promotelinkform.html:107 r2/templates/promotelinkform.html:120 -msgid "weeks" +#: r2/templates/promotedlink.html:63 +msgid "rejected sponsored link" msgstr "" -#: r2/templates/promotelinkform.html:110 r2/templates/promotelinkform.html:113 -msgid "don't expire" +#: r2/templates/promotedlink.html:65 +msgid "queued sponsored link" +msgstr "" + +#: r2/templates/promotedlink.html:67 r2/templates/promotedlink.html:72 +msgid "sponsored link" +msgstr "" + +#: r2/templates/promotedlink.html:69 +msgid "finished sponsored link" +msgstr "" + +#: r2/templates/promotelinkform.html:56 +msgid "create a promotion" +msgstr "" + +#: r2/templates/promotelinkform.html:56 +msgid "edit promotion" +msgstr "" + +#: r2/templates/promotelinkform.html:70 +msgid "NOTE:" +msgstr "" + +#: r2/templates/promotelinkform.html:72 +msgid "" +"you have not set up payment for this promotion. Please click \"set up " +"payment\" to authorize payment." +msgstr "" + +#: r2/templates/promotelinkform.html:75 +msgid "" +"once you set up payment, you will not be charged until the link is approved " +"and scheduled for display" +msgstr "" + +#: r2/templates/promotelinkform.html:83 +msgid "This promotion has been rejected. Please edit and resubmit." +msgstr "" + +#: r2/templates/promotelinkform.html:89 +msgid "" +"NOTE: changes to this promotion will result in its status being reverted to " +"'unapproved'" +msgstr "" + +#: r2/templates/promotelinkform.html:95 +msgid "This promotion is finished. Edits would be a little pointless." msgstr "" #: r2/templates/promotelinkform.html:131 -msgid "thumbnail" +msgid "" +"You'll be able to submit an image for the thumbnail once the promotion is " +"submitted." msgstr "" -#: r2/templates/redditfooter.html:41 +#: r2/templates/promotelinkform.html:354 +msgid "next ->" +msgstr "" + +#: r2/templates/promotelinkform.html:371 +msgid "make this a freebie" +msgstr "" + +#: r2/templates/reddit.html:137 r2/templates/reddit.html:149 +msgid "close this window" +msgstr "" + +#: r2/templates/redditfooter.html:39 #, python-format msgid "" "Use of this site constitutes acceptance of our %(user_agreement)s and " @@ -2342,25 +2364,21 @@ msgstr "" msgid "User Agreement {Genitive}" msgstr "" -#: r2/templates/redditfooter.html:41 +#: r2/templates/redditfooter.html:43 msgid "Privacy Policy {Genitive}" msgstr "" -#: r2/templates/redditfooter.html:42 +#: r2/templates/redditfooter.html:45 #, python-format msgid "(c) %(year)d Conde Nast Digital. All rights reserved." msgstr "" -#: r2/templates/redditfooter.html:65 r2/templates/redditfooter.html:80 -msgid "close this window" -msgstr "" - -#: r2/templates/redditheader.html:58 +#: r2/templates/redditheader.html:55 #, python-format msgid "want to join? %(register)s in seconds" msgstr "" -#: r2/templates/redditheader.html:59 +#: r2/templates/redditheader.html:56 msgid "register" msgstr "" @@ -2426,6 +2444,10 @@ msgstr[1] "" msgid "send this link to" msgstr "" +#: r2/templates/sharelink.html:33 +msgid "your name" +msgstr "" + #: r2/templates/sharelink.html:36 r2/templates/sharelink.html:54 #: r2/templates/sharelink.html:73 msgid "(optional)" @@ -2444,7 +2466,31 @@ msgstr "" msgid "%(user)s from http://%(site)s/ has shared a link with you." msgstr "" -#: r2/templates/subreddit.html:59 r2/templates/subredditinfobar.html:46 +#: r2/templates/shirtpane.html:5 +msgid "sorry. That title is too long to fit on a t-shirt." +msgstr "" + +#: r2/templates/shirtpane.html:37 +msgid "Your reddit headline shirt" +msgstr "" + +#: r2/templates/shirtpane.html:41 +msgid "Color" +msgstr "" + +#: r2/templates/shirtpane.html:48 +msgid "Size" +msgstr "" + +#: r2/templates/shirtpane.html:56 +msgid "Style" +msgstr "" + +#: r2/templates/shirtpane.html:63 +msgid "Qty:" +msgstr "" + +#: r2/templates/subreddit.html:59 r2/templates/subredditinfobar.html:47 #, python-format msgid "a community for %(time)s" msgstr "" @@ -2453,6 +2499,10 @@ msgstr "" msgid "not contributor" msgstr "" +#: r2/templates/subreddit.html:92 +msgid "private" +msgstr "" + #: r2/templates/subreddit.html:96 msgid "over18" msgstr "" @@ -2508,7 +2558,7 @@ msgid "hide the default stylesheet" msgstr "" #: r2/templates/subredditstylesheet.html:83 -#: r2/templates/subredditstylesheet.html:255 +#: r2/templates/subredditstylesheet.html:249 msgid "preview" msgstr "" @@ -2520,45 +2570,45 @@ msgstr "" msgid "images" msgstr "" -#: r2/templates/subredditstylesheet.html:103 +#: r2/templates/subredditstylesheet.html:102 msgid "image file" msgstr "" -#: r2/templates/subredditstylesheet.html:107 +#: r2/templates/subredditstylesheet.html:105 msgid "new image name:" msgstr "" -#: r2/templates/subredditstylesheet.html:114 +#: r2/templates/subredditstylesheet.html:110 msgid "(image names should consist of alphanumeric characters and '-' only)" msgstr "" -#: r2/templates/subredditstylesheet.html:120 +#: r2/templates/subredditstylesheet.html:114 msgid "" "Note: any changes to images here will be reflected immediately on reload and " "cannot be undone." msgstr "" -#: r2/templates/subredditstylesheet.html:204 +#: r2/templates/subredditstylesheet.html:198 msgid "please select an image" msgstr "" -#: r2/templates/subredditstylesheet.html:233 +#: r2/templates/subredditstylesheet.html:227 msgid "paste into stylesheet" msgstr "" -#: r2/templates/subredditstylesheet.html:236 +#: r2/templates/subredditstylesheet.html:230 msgid "delete this image" msgstr "" -#: r2/templates/subredditstylesheet.html:258 +#: r2/templates/subredditstylesheet.html:252 msgid "normal link" msgstr "" -#: r2/templates/subredditstylesheet.html:262 +#: r2/templates/subredditstylesheet.html:256 msgid "compressed link" msgstr "" -#: r2/templates/subredditstylesheet.html:266 +#: r2/templates/subredditstylesheet.html:260 msgid "link with thumbnail" msgstr "" @@ -2578,35 +2628,69 @@ msgstr "" msgid "all-time" msgstr "" -#: r2/templates/usertableitem.html:47 +#: r2/templates/usertableitem.html:50 msgid "remove" msgstr "" -#: r2/templates/utils.html:189 +#: r2/templates/usertext.html:68 +msgid "formatting help" +msgstr "" + +#: r2/templates/usertext.html:68 +msgid "hide help" +msgstr "" + +#: r2/templates/usertext.html:88 +msgid "you type:" +msgstr "" + +#: r2/templates/usertext.html:89 +msgid "you see:" +msgstr "" + +#: r2/templates/usertext.html:92 r2/templates/usertext.html:93 +msgid "italics" +msgstr "" + +#: r2/templates/usertext.html:96 r2/templates/usertext.html:97 +msgid "bold" +msgstr "" + +#: r2/templates/usertext.html:105 r2/templates/usertext.html:106 +#: r2/templates/usertext.html:107 r2/templates/usertext.html:111 +#: r2/templates/usertext.html:112 r2/templates/usertext.html:113 +msgid "item" +msgstr "" + +#: r2/templates/usertext.html:118 r2/templates/usertext.html:119 +msgid "quoted text" +msgstr "" + +#: r2/templates/utils.html:198 msgid "all languages" msgstr "" -#: r2/templates/utils.html:193 +#: r2/templates/utils.html:202 msgid "some languages" msgstr "" -#: r2/templates/utils.html:267 +#: r2/templates/utils.html:266 msgid "uploading" msgstr "" -#: r2/templates/utils.html:269 +#: r2/templates/utils.html:268 msgid "upload" msgstr "" -#: r2/templates/utils.html:366 +#: r2/templates/utils.html:362 msgid "fetching title..." msgstr "" -#: r2/templates/utils.html:367 +#: r2/templates/utils.html:363 msgid "submitting..." msgstr "" -#: r2/templates/utils.html:368 +#: r2/templates/utils.html:364 msgid "loading..." msgstr "" diff --git a/r2/r2/lib/amqp.py b/r2/r2/lib/amqp.py new file mode 100644 index 000000000..6e897a665 --- /dev/null +++ b/r2/r2/lib/amqp.py @@ -0,0 +1,211 @@ +# The contents of this file are subject to the Common Public Attribution +# License Version 1.0. (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +# License Version 1.1, but Sections 14 and 15 have been added to cover use of +# software over a computer network and provide for limited attribution for the +# Original Developer. In addition, Exhibit A has been modified to be consistent +# with Exhibit B. +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +# the specific language governing rights and limitations under the License. +# +# The Original Code is Reddit. +# +# The Original Developer is the Initial Developer. The Initial Developer of the +# Original Code is CondeNet, Inc. +# +# All portions of the code written by CondeNet are Copyright (c) 2006-2009 +# CondeNet, Inc. All Rights Reserved. +################################################################################ + +from threading import local +from datetime import datetime +import os +import sys +import time +import errno +import socket + +from amqplib import client_0_8 as amqp + +from r2.lib.cache import LocalCache +from pylons import g + +amqp_host = g.amqp_host +amqp_user = g.amqp_user +amqp_pass = g.amqp_pass +log = g.log +amqp_virtual_host = g.amqp_virtual_host + +connection = None +channel = local() +have_init = False + +#there are two ways of interacting with this module: add_item and +#handle_items. add_item should only be called from the utils.worker +#thread since it might block for an arbitrary amount of time while +#trying to get a connection amqp. + +def get_connection(): + global connection + global have_init + + while not connection: + try: + connection = amqp.Connection(host = amqp_host, + userid = amqp_user, + password = amqp_pass, + virtual_host = amqp_virtual_host, + insist = False) + except (socket.error, IOError): + print 'error connecting to amqp' + time.sleep(1) + + #don't run init_queue until someone actually needs it. this allows + #the app server to start and serve most pages if amqp isn't + #running + if not have_init: + init_queue() + have_init = True + +def get_channel(reconnect = False): + global connection + global channel + global log + + # Periodic (and increasing with uptime) errors appearing when + # connection object is still present, but appears to have been + # closed. This checks that the the connection is still open. + if connection and connection.channels is None: + log.error("Error: amqp.py, connection object with no available channels. Reconnecting...") + connection = None + + if not connection or reconnect: + channel.chan = None + connection = None + get_connection() + + if not getattr(channel, 'chan', None): + channel.chan = connection.channel() + return channel.chan + +def init_queue(): + from r2.models import admintools + + exchange = 'reddit_exchange' + + chan = get_channel() + + #we'll have one exchange for now + chan.exchange_declare(exchange=exchange, + type='direct', + durable=True, + auto_delete=False) + + #prec_links queue + chan.queue_declare(queue='prec_links', + durable=True, + exclusive=False, + auto_delete=False) + chan.queue_bind(routing_key='prec_links', + queue='prec_links', + exchange=exchange) + + chan.queue_declare(queue='scraper_q', + durable=True, + exclusive=False, + auto_delete=False) + + chan.queue_declare(queue='searchchanges_q', + durable=True, + exclusive=False, + auto_delete=False) + + # new_link + chan.queue_bind(routing_key='new_link', + queue='scraper_q', + exchange=exchange) + chan.queue_bind(routing_key='new_link', + queue='searchchanges_q', + exchange=exchange) + + # new_subreddit + chan.queue_bind(routing_key='new_subreddit', + queue='searchchanges_q', + exchange=exchange) + + # new_comment (nothing here for now) + + # while new items will be put here automatically, we also need a + # way to specify that the item has changed by hand + chan.queue_bind(routing_key='searchchanges_q', + queue='searchchanges_q', + exchange=exchange) + + admintools.admin_queues(chan, exchange) + + +def add_item(routing_key, body, message_id = None): + """adds an item onto a queue. If the connection to amqp is lost it + will try to reconnect and then call itself again.""" + if not amqp_host: + print "Ignoring amqp message %r to %r" % (body, routing_key) + return + + chan = get_channel() + msg = amqp.Message(body, + timestamp = datetime.now(), + delivery_mode = 2) + if message_id: + msg.properties['message_id'] = message_id + + try: + chan.basic_publish(msg, + exchange = 'reddit_exchange', + routing_key = routing_key) + except Exception as e: + if e.errno == errno.EPIPE: + get_channel(True) + add_item(routing_key, body, message_id) + else: + raise + +def handle_items(queue, callback, ack = True, limit = 1, drain = False): + """Call callback() on every item in a particular queue. If the + connection to the queue is lost, it will die. Intended to be + used as a long-running process.""" + + # debuffer stdout so that logging comes through more real-time + sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) + + chan = get_channel() + while True: + msg = chan.basic_get(queue) + if not msg and drain: + return + elif not msg: + time.sleep(1) + continue + + items = [] + #reset the local cache + g.cache.caches = (LocalCache(),) + g.cache.caches[1:] + + while msg: + items.append(msg) + if len(items) >= limit: + break # the innermost loop only + msg = chan.basic_get(queue) + + callback(items) + + if ack: + for item in items: + chan.basic_ack(item.delivery_tag) + +def empty_queue(queue): + """debug function to completely erase the contents of a queue""" + chan = get_channel() + chan.queue_purge(queue) diff --git a/r2/r2/lib/app_globals.py b/r2/r2/lib/app_globals.py index 4583bee85..2bfbca505 100644 --- a/r2/r2/lib/app_globals.py +++ b/r2/r2/lib/app_globals.py @@ -50,7 +50,11 @@ class Globals(object): 'num_serendipity', 'sr_dropdown_threshold', ] - + + float_props = ['min_promote_bid', + 'max_promote_bid', + ] + bool_props = ['debug', 'translator', 'sqlprinting', 'template_debug', @@ -58,8 +62,11 @@ class Globals(object): 'enable_doquery', 'use_query_cache', 'write_query_queue', + 'show_awards', 'css_killswitch', - 'db_create_tables'] + 'db_create_tables', + 'disallow_db_writes', + 'allow_shutdown'] tuple_props = ['memcaches', 'rec_cache', @@ -67,8 +74,10 @@ class Globals(object): 'rendercaches', 'admins', 'sponsors', + # TODO: temporary until we open it up to all users + 'paid_sponsors', 'monitored_servers', - 'default_srs', + 'automatic_reddits', 'agents', 'allowed_css_linked_domains'] @@ -103,14 +112,19 @@ class Globals(object): if not k.startswith("_") and not hasattr(self, k): if k in self.int_props: v = int(v) + elif k in self.float_props: + v = float(v) elif k in self.bool_props: v = self.to_bool(v) elif k in self.tuple_props: v = tuple(self.to_iter(v)) setattr(self, k, v) + self.paid_sponsors = set(x.lower() for x in self.paid_sponsors) + # initialize caches mc = Memcache(self.memcaches, pickleProtocol = 1) + self.memcache = mc self.cache = CacheChain((LocalCache(), mc)) self.permacache = Memcache(self.permacaches, pickleProtocol = 1) self.rendercache = Memcache(self.rendercaches, pickleProtocol = 1) @@ -173,6 +187,11 @@ class Globals(object): self.log.addHandler(logging.StreamHandler()) if self.debug: self.log.setLevel(logging.DEBUG) + else: + self.log.setLevel(logging.WARNING) + + # set log level for pycountry which is chatty + logging.getLogger('pycountry.db').setLevel(logging.CRITICAL) if not self.media_domain: self.media_domain = self.domain @@ -190,26 +209,35 @@ class Globals(object): self.reddit_host = socket.gethostname() self.reddit_pid = os.getpid() + #the shutdown toggle + self.shutdown = False + + #if we're going to use the query_queue, we need amqp + if self.write_query_queue and not self.amqp_host: + raise Exception("amqp_host must be defined to use the query queue") + @staticmethod def to_bool(x): return (x.lower() == 'true') if x else None @staticmethod def to_iter(v, delim = ','): - return (x.strip() for x in v.split(delim)) + return (x.strip() for x in v.split(delim) if x) def load_db_params(self, gc): self.databases = tuple(self.to_iter(gc['databases'])) + self.db_params = {} if not self.databases: return dbm = db_manager.db_manager() - db_params = ('name', 'db_host', 'db_user', 'db_pass', - 'pool_size', 'max_overflow') + db_param_names = ('name', 'db_host', 'db_user', 'db_pass', + 'pool_size', 'max_overflow') for db_name in self.databases: conf_params = self.to_iter(gc[db_name + '_db']) - params = dict(zip(db_params, conf_params)) + params = dict(zip(db_param_names, conf_params)) dbm.engines[db_name] = db_manager.get_engine(**params) + self.db_params[db_name] = params dbm.type_db = dbm.engines[gc['type_db']] dbm.relation_type_db = dbm.engines[gc['rel_type_db']] diff --git a/r2/r2/lib/authorize/__init__.py b/r2/r2/lib/authorize/__init__.py new file mode 100644 index 000000000..87e88763e --- /dev/null +++ b/r2/r2/lib/authorize/__init__.py @@ -0,0 +1,22 @@ +# The contents of this file are subject to the Common Public Attribution +# License Version 1.0. (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +# License Version 1.1, but Sections 14 and 15 have been added to cover use of +# software over a computer network and provide for limited attribution for the +# Original Developer. In addition, Exhibit A has been modified to be consistent +# with Exhibit B. +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +# the specific language governing rights and limitations under the License. +# +# The Original Code is Reddit. +# +# The Original Developer is the Initial Developer. The Initial Developer of the +# Original Code is CondeNet, Inc. +# +# All portions of the code written by CondeNet are Copyright (c) 2006-2009 +# CondeNet, Inc. All Rights Reserved. +################################################################################ +from interaction import * diff --git a/r2/r2/lib/authorize/api.py b/r2/r2/lib/authorize/api.py new file mode 100644 index 000000000..6b4a39434 --- /dev/null +++ b/r2/r2/lib/authorize/api.py @@ -0,0 +1,589 @@ +# The contents of this file are subject to the Common Public Attribution +# License Version 1.0. (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +# License Version 1.1, but Sections 14 and 15 have been added to cover use of +# software over a computer network and provide for limited attribution for the +# Original Developer. In addition, Exhibit A has been modified to be consistent +# with Exhibit B. +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +# the specific language governing rights and limitations under the License. +# +# The Original Code is Reddit. +# +# The Original Developer is the Initial Developer. The Initial Developer of the +# Original Code is CondeNet, Inc. +# +# All portions of the code written by CondeNet are Copyright (c) 2006-2009 +# CondeNet, Inc. All Rights Reserved. +################################################################################ +""" +For talking to authorize.net credit card payments via their XML api. + +This file consists mostly of wrapper classes for dealing with their +API, while the actual useful functions live in interaction.py +""" + +from pylons import g +from httplib import HTTPSConnection +from urlparse import urlparse +import socket, re +from BeautifulSoup import BeautifulStoneSoup +from r2.lib.utils import iters, Storage + +from r2.models import NotFound +from r2.models.bidding import CustomerID, PayID, ShippingAddress + +# list of the most common errors. +Errors = Storage(TESTMODE = "E00009", + TRANSACTION_FAIL = "E00027", + DUPLICATE_RECORD = "E00039", + RECORD_NOT_FOUND = "E00040", + TOO_MANY_PAY_PROFILES = "E00042", + TOO_MANY_SHIP_ADDRESSES = "E00043") + +class AuthorizeNetException(Exception): + pass + + +class SimpleXMLObject(object): + """ + All API transactions are done with authorize.net using XML, so + here's a class for generating and extracting structured data from + XML. + """ + _keys = [] + def __init__(self, **kw): + self._used_keys = self._keys if self._keys else kw.keys() + for k in self._used_keys: + if not hasattr(self, k): + setattr(self, k, kw.get(k, "")) + + @staticmethod + def simple_tag(name, content, **attrs): + attrs = " ".join('%s="%s"' % (k, v) for k, v in attrs.iteritems()) + if attrs: attrs = " " + attrs + return ("<%(name)s%(attrs)s>%(content)s" % + dict(name = name, content = content, attrs = attrs)) + + def toXML(self): + content = [] + def process(k, v): + if isinstance(v, SimpleXMLObject): + v = v.toXML() + if v is not None: + content.append(self.simple_tag(k, v)) + + for k in self._used_keys: + v = getattr(self, k) + if isinstance(v, iters): + for val in v: + process(k, val) + else: + process(k, v) + return self._wrapper("".join(content)) + + @classmethod + def fromXML(cls, data): + kw = {} + for k in cls._keys: + d = data.find(k.lower()) + if d and d.contents: + kw[k] = unicode(d.contents[0]) + return cls(**kw) + + + def __repr__(self): + return "<%s {%s}>" % (self.__class__.__name__, + ",".join("%s=%s" % (k, repr(getattr(self, k))) + for k in self._used_keys)) + + def _name(self): + name = self.__class__.__name__ + return name[0].lower() + name[1:] + + def _wrapper(self, content): + return content + +class Auth(SimpleXMLObject): + _keys = ["name", "transactionKey"] + +class Address(SimpleXMLObject): + _keys = ["firstName", "lastName", "company", "address", + "city", "state", "zip", "country", "phoneNumber", + "faxNumber", + "customerPaymentProfileId", + "customerAddressId" ] + def __init__(self, **kw): + kw['customerPaymentProfileId'] = kw.get("customerPaymentProfileId", + None) + kw['customerAddressId'] = kw.get("customerAddressId", None) + SimpleXMLObject.__init__(self, **kw) + + +class CreditCard(SimpleXMLObject): + _keys = ["cardNumber", "expirationDate", "cardCode"] + + + +class Profile(SimpleXMLObject): + """ + Converts a user into a Profile object. + """ + _keys = ["merchantCustomerId", "description", + "email", "customerProfileId", "paymentProfiles", "shipToList", + "validationMode"] + def __init__(self, user, paymentProfiles, address, + validationMode = None): + SimpleXMLObject.__init__(self, merchantCustomerId = user._fullname, + description = user.name, email = "", + paymentProfiles = paymentProfiles, + shipToList = address, + validationMode = validationMode, + customerProfileId=CustomerID.get_id(user)) + + +class PaymentProfile(SimpleXMLObject): + _keys = ["billTo", "payment", "customerPaymentProfileId", "validationMode"] + def __init__(self, billTo, card, paymentId = None, + validationMode = None): + SimpleXMLObject.__init__(self, billTo = billTo, + customerPaymentProfileId = paymentId, + payment = SimpleXMLObject(creditCard = card), + validationMode = validationMode) + + @classmethod + def fromXML(cls, res): + payid = int(res.customerpaymentprofileid.contents[0]) + return cls(Address.fromXML(res.billto), + CreditCard.fromXML(res.payment), payid) + +class Order(SimpleXMLObject): + _keys = ["invoiceNumber", "description", "purchaseOrderNumber"] + +class Transaction(SimpleXMLObject): + _keys = ["amount", "customerProfileId", "customerPaymentProfileId", + "transId", "order"] + + def __init__(self, amount, profile_id, pay_id, trans_id = None, + order = None): + SimpleXMLObject.__init__(self, amount = amount, + customerProfileId = profile_id, + customerPaymentProfileId = pay_id, + transId = trans_id, + order = order) + + def _wrapper(self, content): + return self.simple_tag(self._name(), content) + +# authorize and charge +class ProfileTransAuthCapture(Transaction): pass + +# only authorize (no charge is made) +class ProfileTransAuthOnly(Transaction): pass +# charge only (requires previous auth_only) +class ProfileTransPriorAuthCapture(Transaction): pass +# stronger than above: charge even on decline (not sure why you would want to) +class ProfileTransCaptureOnly(Transaction): pass +# refund a transaction +class ProfileTransRefund(Transaction): pass +# void a transaction +class ProfileTransVoid(Transaction): pass + + + +#----- +class AuthorizeNetRequest(SimpleXMLObject): + _keys = ["merchantAuthentication"] + + @property + def merchantAuthentication(self): + return Auth(name = g.authorizenetname, + transactionKey = g.authorizenetkey) + + def _wrapper(self, content): + return ('' + + self.simple_tag(self._name(), content, + xmlns="AnetApi/xml/v1/schema/AnetApiSchema.xsd")) + + def make_request(self): + g.log.error("Authorize.net request:") + g.log.error(self.toXML()) + u = urlparse(g.authorizenetapi) + try: + conn = HTTPSConnection(u.hostname, u.port) + conn.request("POST", u.path, self.toXML(), + {"Content-type": "text/xml"}) + res = conn.getresponse() + res = self.handle_response(res.read()) + conn.close() + return res + except socket.error: + return False + + def is_error_code(self, res, code): + return (res.message.code and res.message.code.contents and + res.message.code.contents[0] == code) + + + def process_error(self, res): + raise AuthorizeNetException, res + + _autoclose_re = re.compile("<([^/]+)/>") + def _autoclose_handler(self, m): + return "<%(m)s>" % dict(m = m.groups()[0]) + + def handle_response(self, res): + print "RESPONSE:" + print res + res = self._autoclose_re.sub(self._autoclose_handler, res) + res = BeautifulStoneSoup(res, markupMassage=False) + if res.resultcode.contents[0] == u"Ok": + return self.process_response(res) + else: + return self.process_error(res) + + def process_response(self, res): + raise NotImplementedError + +class CustomerRequest(AuthorizeNetRequest): + _keys = AuthorizeNetRequest._keys + ["customerProfileId"] + def __init__(self, user, **kw): + if isinstance(user, int): + cust_id = user + self._user = None + else: + cust_id = CustomerID.get_id(user) + self._user = user + AuthorizeNetRequest.__init__(self, customerProfileId = cust_id, **kw) + + +# --- real request classes below + +class CreateCustomerProfileRequest(AuthorizeNetRequest): + """ + Create a new user object on authorize.net and return the new object ID. + + Handles the case of already existing users on either end + gracefully and will update the Account object accordingly. + """ + _keys = AuthorizeNetRequest._keys + ["profile", "validationMode"] + + def __init__(self, user, validationMode = None): + # cache the user object passed in + self._user = user + AuthorizeNetRequest.__init__(self, + profile = Profile(user, None, None), + validationMode = validationMode) + + def process_response(self, res): + customer_id = int(res.customerprofileid.contents[0]) + CustomerID.set(self._user, customer_id) + return customer_id + + def make_request(self): + # don't send a new request if the user already has an id + return (CustomerID.get_id(self._user) or + AuthorizeNetRequest.make_request(self)) + + re_lost_id = re.compile("A duplicate record with id (\d+) already exists") + def process_error(self, res): + if self.is_error_code(res, Errors.DUPLICATE_RECORD): + # D'oh. We lost one + m = self.re_lost_id.match(res.find("text").contents[0]).groups() + CustomerID.set(self._user, m[0]) + # otherwise, we might have sent a user that already had a customer ID + cust_id = CustomerID.get_id(self._user) + if cust_id: + return cust_id + return AuthorizeNetRequest.process_error(self, res) + +class CreateCustomerPaymentProfileRequest(CustomerRequest): + """ + Adds a payment profile to an existing user object. The profile + includes a valid address and a credit card number. + """ + _keys = (CustomerRequest._keys + ["paymentProfile", "validationMode"]) + + def __init__(self, user, address, creditcard, validationMode = None): + CustomerRequest.__init__(self, user, + paymentProfile = PaymentProfile(address, + creditcard), + validationMode = validationMode) + + def process_response(self, res): + pay_id = int(res.customerpaymentprofileid.contents[0]) + PayID.add(self._user, pay_id) + return pay_id + + def process_error(self, res): + if self.is_error_code(res, Errors.DUPLICATE_RECORD): + u, data = GetCustomerProfileRequest(self._user).make_request() + profiles = data.paymentProfiles + if len(profiles) == 1: + return profiles[0].customerPaymentProfileId + return + return CustomerRequest.process_error(self,res) + +class CreateCustomerShippingAddressRequest(CustomerRequest): + """ + Adds a shipping address. + """ + _keys = CustomerRequest._keys + ["address"] + def process_response(self, res): + pay_id = int(res.customeraddressid.contents[0]) + ShippingAddress.add(self._user, pay_id) + return pay_id + + def process_error(self, res): + if self.is_error_code(res, Errors.DUPLICATE_RECORD): + return + return CustomerRequest.process_error(self, res) + + +class GetCustomerPaymentProfileRequest(CustomerRequest): + _keys = CustomerRequest._keys + ["customerPaymentProfileId"] + """ + Gets a payment profile by user Account object and authorize.net + profileid of the payment profile. + + Error handling: make_request returns None if the id generates a + RECORD_NOT_FOUND error from the server. The user object is + cleaned up in either case; if the user object lacked the (valid) + pay id, it is added to its list, while if the pay id is invalid, + it is removed from the user object. + """ + def __init__(self, user, profileid): + CustomerRequest.__init__(self, user, + customerPaymentProfileId=profileid) + def process_response(self, res): + # add the id to the user object in case something has gone wrong + PayID.add(self._user, self.customerPaymentProfileId) + return PaymentProfile.fromXML(res.paymentprofile) + + def process_error(self, res): + if self.is_error_code(res, Errors.RECORD_NOT_FOUND): + PayID.delete(self._user, self.customerPaymentProfileId) + return CustomerRequest.process_error(self,res) + + +class GetCustomerShippingAddressRequest(CustomerRequest): + """ + Same as GetCustomerPaymentProfileRequest except with shipping addresses. + + Error handling is identical. + """ + _keys = CustomerRequest._keys + ["customerAddressId"] + def __init__(self, user, shippingid): + CustomerRequest.__init__(self, user, + customerAddressId=shippingid) + + def process_response(self, res): + # add the id to the user object in case something has gone wrong + ShippingAddress.add(self._user, self.customerAddressId) + return Address.fromXML(res.address) + + def process_error(self, res): + if self.is_error_code(res, Errors.RECORD_NOT_FOUND): + ShippingAddress.delete(self._user, self.customerAddressId) + return CustomerRequest.process_error(self,res) + +class GetCustomerProfileIdsRequest(AuthorizeNetRequest): + """ + Get a list of all customer ids that have been recorded with + authorize.net + """ + def process_response(self, res): + return [int(x.contents[0]) for x in res.ids.findAll('numericstring')] + +class GetCustomerProfileRequest(CustomerRequest): + """ + Given a user, find their customer information. + """ + def process_response(self, res): + from r2.models import Account + fullname = res.merchantcustomerid.contents[0] + name = res.description.contents[0] + customer_id = int(res.customerprofileid.contents[0]) + acct = Account._by_name(name) + + # make sure we are updating the correct account! + if acct.name == name: + CustomerID.set(acct, customer_id) + else: + raise AuthorizeNetException, \ + "account name doesn't match authorize.net account" + + # parse the ship-to list, and make sure the Account is up todate + ship_to = [] + for profile in res.findAll("shiptolist"): + a = Address.fromXML(profile) + ShippingAddress.add(acct, a.customerAddressId) + ship_to.append(a) + + # parse the payment profiles, and ditto + profiles = [] + for profile in res.findAll("paymentprofiles"): + a = Address.fromXML(profile) + cc = CreditCard.fromXML(profile.payment) + payprof = PaymentProfile(a, cc,int(a.customerPaymentProfileId)) + PayID.add(acct, a.customerPaymentProfileId) + profiles.append(payprof) + + return acct, Profile(acct, profiles, ship_to) + +class DeleteCustomerProfileRequest(CustomerRequest): + """ + Delete a customer shipping address + """ + def process_response(self, res): + if self._user: + CustomerID.delete(self._user) + return + + def process_error(self, res): + if self.is_error_code(res, Errors.RECORD_NOT_FOUND): + CustomerID.delete(self._user) + return CustomerRequest.process_error(self,res) + +class DeleteCustomerPaymentProfileRequest(GetCustomerPaymentProfileRequest): + """ + Delete a customer shipping address + """ + def process_response(self, res): + PayID.delete(self._user,self.customerPaymentProfileId) + return True + + def process_error(self, res): + if self.is_error_code(res, Errors.RECORD_NOT_FOUND): + PayID.delete(self._user,self.customerPaymentProfileId) + return GetCustomerPaymentProfileRequest.process_error(self,res) + +class DeleteCustomerShippingAddressRequest(GetCustomerShippingAddressRequest): + """ + Delete a customer shipping address + """ + def process_response(self, res): + ShippingAddress.delete(self._user, self.customerAddressId) + return True + + def process_error(self, res): + if self.is_error_code(res, Errors.RECORD_NOT_FOUND): + ShippingAddress.delete(self._user, self.customerAddressId) + GetCustomerShippingAddressRequest.process_error(self,res) + + + + +# TODO +#class UpdateCustomerProfileRequest(AuthorizeNetRequest): +# _keys = (AuthorizeNetRequest._keys + ["profile"]) +# +# def __init__(self, user): +# profile = Profile(user, None, None) +# AuthorizeNetRequest.__init__(self, profile = profile) + +class UpdateCustomerPaymentProfileRequest(CreateCustomerPaymentProfileRequest): + """ + For updating the user's payment profile + """ + def __init__(self, user, paymentid, address, creditcard, + validationMode = None): + CustomerRequest.__init__(self, user, + paymentProfile=PaymentProfile(address, + creditcard, + paymentid), + validationMode = validationMode) + + def process_response(self, res): + return self.paymentProfile.customerPaymentProfileId + + +class UpdateCustomerShippingAddressRequest( + CreateCustomerShippingAddressRequest): + """ + For updating the user's shipping address + """ + def __init__(self, user, address_id, address): + address.customerAddressId = address_id + CreateCustomerShippingAddressRequest.__init__(self, user, + address = address) + + def process_response(self, res): + return True + + + + +class CreateCustomerProfileTransactionRequest(AuthorizeNetRequest): + _keys = AuthorizeNetRequest._keys + ["transaction", "extraOptions"] + + # unlike every other response we get back, this api function + # returns CSV data of the response with no field labels. these + # are used in package_response to zip this data into a usable + # storage. + response_keys = ("response_code", + "response_subcode", + "response_reason_code", + "response_reason_text", + "authorization_code", + "avs_response", + "trans_id", + "invoice_number", + "description", + "amount", "method", + "transaction_type", + "customerID", + "firstName", "lastName", + "company", "address", "city", "state", + "zip", "country", + "phoneNumber", "faxNumber", "email", + "shipTo_firstName", "shipTo_lastName", + "shipTo_company", "shipTo_address", + "shipTo_city", "shipTo_state", + "shipTo_zip", "shipTo_country", + "tax", "duty", "freight", + "tax_exempt", "po_number", "md5", + "cav_response") + + # list of casts for the response fields given above + response_types = dict(response_code = int, + response_subcode = int, + response_reason_code = int, + trans_id = int) + + def __init__(self, **kw): + from pylons import g + self._extra = kw.get("extraOptions", {}) + #if g.debug: + # self._extra['x_test_request'] = "TRUE" + AuthorizeNetRequest.__init__(self, **kw) + + @property + def extraOptions(self): + return "" % "&".join("%s=%s" % x + for x in self._extra.iteritems()) + + def process_response(self, res): + return (True, self.package_response(res)) + + def process_error(self, res): + if self.is_error_code(res, Errors.TRANSACTION_FAIL): + return (False, self.package_response(res)) + elif self.is_error_code(res, Errors.TESTMODE): + return (None, None) + return AuthorizeNetRequest.process_error(self,res) + + + def package_response(self, res): + content = res.directresponse.contents[0] + s = Storage(zip(self.response_keys, content.split(','))) + for name, cast in self.response_types.iteritems(): + try: + s[name] = cast(s[name]) + except ValueError: + pass + return s + diff --git a/r2/r2/lib/authorize/interaction.py b/r2/r2/lib/authorize/interaction.py new file mode 100644 index 000000000..b0feb6d5e --- /dev/null +++ b/r2/r2/lib/authorize/interaction.py @@ -0,0 +1,176 @@ +# The contents of this file are subject to the Common Public Attribution +# License Version 1.0. (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +# License Version 1.1, but Sections 14 and 15 have been added to cover use of +# software over a computer network and provide for limited attribution for the +# Original Developer. In addition, Exhibit A has been modified to be consistent +# with Exhibit B. +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +# the specific language governing rights and limitations under the License. +# +# The Original Code is Reddit. +# +# The Original Developer is the Initial Developer. The Initial Developer of the +# Original Code is CondeNet, Inc. +# +# All portions of the code written by CondeNet are Copyright (c) 2006-2009 +# CondeNet, Inc. All Rights Reserved. +################################################################################ +from api import * +from pylons import g +from r2.models.bidding import Bid + +# useful test data: +test_card = dict(AMEX = ("370000000000002" , 1234), + DISCOVER = ("6011000000000012" , 123), + MASTERCARD = ("5424000000000015" , 123), + VISA = ("4007000000027" , 123), + # visa card which generates error codes based on the amount + ERRORCARD = ("4222222222222" , 123)) + +test_card = Storage((k, CreditCard(cardNumber = x, expirationDate="2011-11", + cardCode = y)) for k, (x, y) in + test_card.iteritems()) + +test_address = Address(firstName = "John", lastName = "Doe", + address = "123 Fake St.", + city = "Anytown", state = "MN", zip = "12346") + +def get_account_info(user, recursed = False): + # if we don't have an ID for the user, try to make one + if not CustomerID.get_id(user): + cust_id = CreateCustomerProfileRequest(user).make_request() + + # if we do have a customerid, we should be able to fetch it from authorize + try: + u, data = GetCustomerProfileRequest(user).make_request() + except AuthorizeNetException: + u = None + + # if the user and the returned user don't match, delete the + # current customer_id and recurse + if u != user: + if not recursed: + CustomerID.delete(user) + return get_account_info(user, True) + else: + raise AuthorizeNetException, "error creating user" + return data + +def edit_profile(user, address, creditcard, pay_id = None): + if pay_id: + return UpdateCustomerPaymentProfileRequest( + user, pay_id, address, creditcard).make_request() + else: + return CreateCustomerPaymentProfileRequest( + user, address, creditcard).make_request() + + + + +def _make_transaction(trans_cls, amount, user, pay_id, + order = None, trans_id = None, test = None): + """ + private function for handling transactions (since the data is + effectively the same regardless of trans_cls) + """ + # format the amount + if amount: + amount = "%.2f" % amount + # lookup customer ID + cust_id = CustomerID.get_id(user) + # create a new transaction + trans = trans_cls(amount, cust_id, pay_id, trans_id = trans_id, + order = order) + extra = {} + # the optional test field makes the transaction a test, and will + # make the response be the error code corresponding to int(test). + if isinstance(test, int): + extra = dict(x_test_request = "TRUE", + x_card_num = test_card.ERRORCARD.cardNumber, + x_amount = test) + + # using the transaction, generate a transaction request and make it + req = CreateCustomerProfileTransactionRequest(transaction = trans, + extraOptions = extra) + return req.make_request() + + +def auth_transaction(amount, user, payid, thing, test = None): + # use negative pay_ids to identify freebies, coupons, or anything + # that doesn't require a CC. + if payid < 0: + trans_id = -thing._id + # update previous freebie transactions if we can + try: + bid = Bid.one(thing_id = thing._id, + pay_id = payid) + bid.bid = amount + bid.auth() + except NotFound: + bid = Bid._new(trans_id, user, payid, thing._id, amount) + return bid.transaction + + elif int(payid) in PayID.get_ids(user): + order = Order(invoiceNumber = "%dT%d" % (user._id, thing._id)) + success, res = _make_transaction(ProfileTransAuthOnly, + amount, user, payid, + order = order, test = test) + if success: + Bid._new(res.trans_id, user, payid, thing._id, amount) + return res.trans_id + elif res is None: + # we are in test mode! + return auth_transaction(amount, user, -1, thing, test = test) + # duplicate transaction, which is bad, but not horrible. Log + # the transaction id, creating a new bid if necessary. + elif (res.response_code, res.response_reason_code) == (3,11): + try: + Bid.one(res.trans_id) + except NotFound: + Bid._new(res.trans_id, user, payid, thing._id, amount) + return res.trans_id + + + +def void_transaction(user, trans_id, test = None): + bid = Bid.one(trans_id) + bid.void() + # verify that the transaction has the correct ownership + if bid.account_id == user._id and trans_id > 0: + res = _make_transaction(ProfileTransVoid, + None, user, None, trans_id = trans_id, + test = test) + return res + + +def charge_transaction(user, trans_id, test = None): + bid = Bid.one(trans_id) + bid.charged() + if trans_id < 0: + # freebies are automatically approved + return True + elif bid.account_id == user._id: + res = _make_transaction(ProfileTransPriorAuthCapture, + bid.bid, user, + bid.pay_id, trans_id = trans_id, + test = test) + return bool(res) + + +def refund_transaction(amount, user, trans_id, test = None): + bid = Bid.one(trans_id) + if trans_id > 0: + # create a new bid to identify the refund + success, res = _make_transaction(ProfileTransRefund, + amount, user, bid.pay_id, + trans_id = trans_id, + test = test) + if success: + bid = Bid._new(res.trans_id, user, -1, bid.thing_id, amount) + bid.refund() + return bool(res.trans_id) + diff --git a/r2/r2/lib/base.py b/r2/r2/lib/base.py index 2852585de..039ad867f 100644 --- a/r2/r2/lib/base.py +++ b/r2/r2/lib/base.py @@ -90,6 +90,8 @@ class BaseController(WSGIController): request.environ['pylons.routes_dict']['action'] = \ meth + '_' + action + c.thread_pool = environ['paste.httpserver.thread_pool'] + c.response = Response() res = WSGIController.__call__(self, environ, start_response) return res diff --git a/r2/r2/lib/contrib/nymph.py b/r2/r2/lib/contrib/nymph.py new file mode 100644 index 000000000..0ad17760e --- /dev/null +++ b/r2/r2/lib/contrib/nymph.py @@ -0,0 +1,93 @@ +# The contents of this file are subject to the Common Public Attribution +# License Version 1.0. (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +# License Version 1.1, but Sections 14 and 15 have been added to cover use of +# software over a computer network and provide for limited attribution for the +# Original Developer. In addition, Exhibit A has been modified to be consistent +# with Exhibit B. +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +# the specific language governing rights and limitations under the License. +# +# The Original Code is Reddit. +# +# The Original Developer is the Initial Developer. The Initial Developer of the +# Original Code is CondeNet, Inc. +# +# All portions of the code written by CondeNet are Copyright (c) 2006-2009 +# CondeNet, Inc. All Rights Reserved. +################################################################################ +import re, sys, Image, os, hashlib, StringIO + + +class Spriter(object): + spritable = re.compile(r" *background-image: *url\((.*)\) *.*/\* *SPRITE *\*/") + + def __init__(self, padding = (4, 4), + css_path = '/static/', actual_path = "r2/public/static/"): + self.images = [] + self.im_lookup = {} + self.ypos = [0] + self.padding = padding + + self.css_path = css_path + self.actual_path = actual_path + + def make_sprite(self, line): + path, = self.spritable.findall(line) + path = re.sub("^" + self.css_path, self.actual_path, path) + if os.path.exists(path): + if path in self.im_lookup: + i = self.im_lookup[path] + else: + im = Image.open(path) + self.images.append(im) + self.im_lookup[path] = len(self.images) - 1 + self.ypos.append(self.ypos[-1] + im.size[1] + + 2 * self.padding[1]) + i = len(self.images) - 1 + return "\n".join([" background-image: url(%(sprite)s);", + " background-position: %dpx %spx;" % + (-self.padding[0], "%(pos_" + str(i) + ")s"), + ""]) + return line + + def finish(self, out_file, out_string): + width = 2 * self.padding[0] + max(i.size[0] for i in self.images) + height = sum((i.size[1] + 2 * self.padding[1]) for i in self.images) + + master = Image.new(mode = "RGBA", size = (width, height), + color = (0,0,0,0)) + + for i, image in enumerate(self.images): + master.paste(image, + (self.padding[0], self.padding[1] + self.ypos[i])) + + master.save(os.path.join(self.actual_path, out_file)) + + d = dict(('pos_' + str(i), -self.padding[1] - y) + for i, y in enumerate(self.ypos)) + + h = hashlib.md5(master.tostring()).hexdigest() + d['sprite'] = os.path.join(self.css_path, "%s?v=%s" % (out_file, h)) + + return out_string % d + +def process_css(incss, out_file = 'sprite.png', css_path = "/static/"): + s = Spriter(css_path = css_path) + out = StringIO.StringIO() + + with open(incss, 'r') as handle: + for line in handle: + if s.spritable.match(line): + out.write(s.make_sprite(line)) + else: + out.write(line.replace('%', '%%')) + + return s.finish(out_file, out.getvalue()) + +if __name__ == '__main__': + import sys + print process_css(sys.argv[-1]) diff --git a/r2/r2/lib/cssfilter.py b/r2/r2/lib/cssfilter.py index d79c9d5c8..00541ad69 100644 --- a/r2/r2/lib/cssfilter.py +++ b/r2/r2/lib/cssfilter.py @@ -365,7 +365,7 @@ def clean_image(data,format): return ret -def save_sr_image(sr, data, num = None): +def save_sr_image(sr, data, resource = None): """ uploades image data to s3 as a PNG and returns its new url. Urls will be of the form: @@ -383,11 +383,12 @@ def save_sr_image(sr, data, num = None): f.write(data) f.flush() - resource = g.s3_thumb_bucket + sr._fullname - if num is not None: - resource += '_' + str(num) - resource += '.png' - + if resource is not None: + resource = "_%s" % resource + else: + resource = "" + resource = g.s3_thumb_bucket + sr._fullname + resource + ".png" + s3cp.send_file(f.name, resource, 'image/png', 'public-read', None, False) finally: diff --git a/r2/r2/lib/db/queries.py b/r2/r2/lib/db/queries.py index 72b6557ee..b77f7a8cf 100644 --- a/r2/r2/lib/db/queries.py +++ b/r2/r2/lib/db/queries.py @@ -4,10 +4,11 @@ from r2.lib.db.thing import Thing, Merge from r2.lib.db.operators import asc, desc, timeago from r2.lib.db import query_queue from r2.lib.db.sorts import epoch_seconds -from r2.lib.utils import fetch_things2, worker, tup +from r2.lib.utils import fetch_things2, worker, tup, UniqueIterator from r2.lib.solrsearch import DomainSearchQuery from datetime import datetime +import itertools from pylons import g query_cache = g.permacache @@ -82,26 +83,38 @@ class CachedResults(object): return tuple(lst) def can_insert(self): - """True if a new item can just be inserted, which is when the - query is only sorted by date.""" - return self.query._sort == [desc('_date')] + """True if a new item can just be inserted rather than + rerunning the query. This is only true in some + circumstances, which includes having no time rules, and + being sorted descending""" + if self.query._sort in ([desc('_date')], + [desc('_hot'), desc('_date')], + [desc('_score'), desc('_date')], + [desc('_controversy'), desc('_date')]): + if not any(r.lval.name == '_date' + for r in self.query._rules): + # if no time-rule is specified, then it's 'all' + return True + return False def can_delete(self): """True if a item can be removed from the listing, always true for now.""" return True def insert(self, items): - """Inserts the item at the front of the cached data. Assumes the query - is sorted by date descending""" + """Inserts the item into the cached data. This only works + under certain criteria, see can_insert.""" self.fetch() t = [ self.make_item_tuple(item) for item in tup(items) ] - if len(t) == 1 and self.data and t[0][1:] > self.data[0][1:]: - self.data.insert(0, t[0]) - else: - self.data.extend(t) - self.data = list(set(self.data)) - self.data.sort(key=lambda x: x[1:], reverse=True) + # insert the new items, remove the duplicates (keeping the one + # being inserted over the stored value if applicable), and + # sort the result + data = itertools.chain(t, self.data) + data = UniqueIterator(data, key = lambda x: x[0]) + data = sorted(data, key=lambda x: x[1:], reverse=True) + data = list(data) + self.data = data query_cache.set(self.iden, self.data[:precompute_limit]) @@ -316,6 +329,7 @@ def add_queries(queries, insert_items = None, delete_items = None): log('Adding precomputed query %s' % q) query_queue.add_query(q) worker.do(_add_queries) + #can be rewritten to be more efficient def all_queries(fn, obj, *param_lists): @@ -381,12 +395,12 @@ def new_vote(vote): if not isinstance(item, Link): return - if vote.valid_thing: + if vote.valid_thing and not item._spam and not item._deleted: sr = item.subreddit_slow - results = all_queries(get_links, sr, ('hot', 'new'), ['all']) + results = [get_links(sr, 'hot', 'all')] results.extend(all_queries(get_links, sr, ('top', 'controversial'), db_times.keys())) #results.append(get_links(sr, 'toplinks', 'all')) - add_queries(results) + add_queries(results, insert_items = item) #must update both because we don't know if it's a changed vote if vote._name == '1': @@ -443,7 +457,14 @@ def ban(things): if links: add_queries([get_spam_links(sr)], insert_items = links) - add_queries([get_links(sr, 'new', 'all')], delete_items = links) + # rip it out of the listings. bam! + results = [get_links(sr, 'hot', 'all'), + get_links(sr, 'new', 'all'), + get_links(sr, 'top', 'all'), + get_links(sr, 'controversial', 'all')] + results.extend(all_queries(get_links, sr, ('top', 'controversial'), db_times.keys())) + add_queries(results, delete_items = links) + #if comments: # add_queries([get_spam_comments(sr)], insert_items = comments) @@ -459,8 +480,14 @@ def unban(things): if links: add_queries([get_spam_links(sr)], delete_items = links) - # put it back in 'new' - add_queries([get_links(sr, 'new', 'all')], insert_items = links) + # put it back in the listings + results = [get_links(sr, 'hot', 'all'), + get_links(sr, 'new', 'all'), + get_links(sr, 'top', 'all'), + get_links(sr, 'controversial', 'all')] + results.extend(all_queries(get_links, sr, ('top', 'controversial'), db_times.keys())) + add_queries(results, insert_items = links) + #if comments: # add_queries([get_spam_comments(sr)], delete_items = comments) diff --git a/r2/r2/lib/db/query_queue.py b/r2/r2/lib/db/query_queue.py index c41a171e5..f1c42e38d 100644 --- a/r2/r2/lib/db/query_queue.py +++ b/r2/r2/lib/db/query_queue.py @@ -1,117 +1,51 @@ -from __future__ import with_statement -from r2.lib.workqueue import WorkQueue -from r2.lib.db.tdb_sql import make_metadata, create_table, index_str - import cPickle as pickle from datetime import datetime -from urllib2 import Request, urlopen -from urllib import urlencode -from threading import Lock -import time -import sqlalchemy as sa -from sqlalchemy.exceptions import SQLError +from r2.lib import amqp from pylons import g -tz = g.tz -#the current -running = set() -running_lock = Lock() - -def make_query_queue_table(): - engine = g.dbm.engines['query_queue'] - metadata = make_metadata(engine) - table = sa.Table(g.db_app_name + '_query_queue', metadata, - sa.Column('iden', sa.String, primary_key = True), - sa.Column('query', sa.Binary), - sa.Column('date', sa.DateTime(timezone = True))) - date_idx = index_str(table, 'date', 'date') - create_table(table, [date_idx]) - return table - -query_queue_table = make_query_queue_table() +working_prefix = 'working_' +prefix = 'prec_link_' +TIMEOUT = 120 def add_query(cached_results): - """Adds a CachedResults instance to the queue db, ignoring duplicates""" - d = dict(iden = cached_results.query._iden(), - query = pickle.dumps(cached_results, -1), - date = datetime.now(tz)) - try: - query_queue_table.insert().execute(d) - except SQLError, e: - #don't worry about inserting duplicates - if not 'IntegrityError' in str(e): - raise - -def remove_query(iden): - """Removes a row identified with iden from the query queue. To be - called after a CachedResults is updated.""" - table = query_queue_table - d = table.delete(table.c.iden == iden) - d.execute() - -def get_query(): - """Gets the next query off the queue, ignoring the currently running - queries.""" - table = query_queue_table - - s = table.select(order_by = sa.asc(table.c.date), limit = 1) - s.append_whereclause(sa.and_(*[table.c.iden != i for i in running])) - r = s.execute().fetchone() - - if r: - return r.iden, r.query - else: - return None, None - -def make_query_job(iden, pickled_cr): - """Creates a job to send to the query worker. Updates a cached result - then removes the query from both the queue and the running set. If - sending the job fails, the query is only remove from the running - set.""" - precompute_worker = g.query_queue_worker - def job(): - try: - finished = False - r = Request(url = precompute_worker + '/doquery', - data = urlencode([('query', pickled_cr)]), - #this header prevents pylons from turning the - #parameter into unicode, which breaks pickling - headers = {'x-dont-decode':'true'}) - urlopen(r) - finished = True - finally: - with running_lock: - running.remove(iden) - #if finished is false, we'll leave the query in the db - #so we can try again later (e.g. in the event the - #worker is down) - if finished: - remove_query(iden) - return job + amqp.add_item('prec_links', pickle.dumps(cached_results, -1)) def run(): - """Pull jobs from the queue, creates a job, and sends them to a - WorkQueue for processing.""" - num_workers = g.num_query_queue_workers - wq = WorkQueue(num_workers = num_workers) - wq.start() + def callback(msgs): + for msg in msgs: # will be len==1 + # r2.lib.db.queries.CachedResults + cr = pickle.loads(msg.body) + iden = cr.query._iden() - while True: - job = None - #limit the total number of jobs in the WorkQueue. we don't - #need to load the entire db queue right away (the db queue can - #get quite large). - if len(running) < 2 * num_workers: - with running_lock: - iden, pickled_cr = get_query() - if pickled_cr is not None: - if not iden in running: - running.add(iden) - job = make_query_job(iden, pickled_cr) - wq.add(job) + working_key = working_prefix + iden + key = prefix + iden - #if we didn't find a job, sleep before trying again - if not job: - time.sleep(1) + last_time = g.memcache.get(key) + # check to see if we've computed this job since it was + # added to the queue + if last_time and last_time > msg.timestamp: + print 'skipping, already computed ', key + return + + # check if someone else is working on this + elif not g.memcache.add(working_key, 1, TIMEOUT): + print 'skipping, someone else is working', working_key + return + + cr = pickle.loads(msg.body) + + print 'working: ', iden, cr.query._rules + start = datetime.now() + cr.update() + done = datetime.now() + q_time_s = (done - msg.timestamp).seconds + proc_time_s = (done - start).seconds + ((done - start).microseconds/1000000.0) + print ('processed %s in %.6f seconds after %d seconds in queue' + % (iden, proc_time_s, q_time_s)) + + g.memcache.set(key, datetime.now()) + g.memcache.delete(working_key) + + amqp.handle_items('prec_links', callback, limit = 1) diff --git a/r2/r2/lib/db/sorts.py b/r2/r2/lib/db/sorts.py index 6d164618d..655984bc0 100644 --- a/r2/r2/lib/db/sorts.py +++ b/r2/r2/lib/db/sorts.py @@ -19,7 +19,7 @@ # All portions of the code written by CondeNet are Copyright (c) 2006-2009 # CondeNet, Inc. All Rights Reserved. ################################################################################ -from math import log +from math import log, sqrt from datetime import datetime, timedelta from pylons import g @@ -46,3 +46,28 @@ def controversy(ups, downs): """The controversy sort.""" return float(ups + downs) / max(abs(score(ups, downs)), 1) +def _confidence(ups, downs): + """The confidence sort. + http://www.evanmiller.org/how-not-to-sort-by-average-rating.html""" + n = float(ups + downs) + if n == 0: + return 0 + z = 1.0 #1.0 = 85%, 1.6 = 95% + phat = float(ups) / n + return sqrt(phat+z*z/(2*n)-z*((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n) + + +up_range = 400 +down_range = 100 +confidences = [] +for ups in xrange(up_range): + for downs in xrange(down_range): + confidences.append(_confidence(ups, downs)) + +def confidence(ups, downs): + if ups + downs == 0: + return 0 + elif ups < up_range and downs < down_range: + return confidences[downs + ups * down_range] + else: + return _confidence(ups, downs) diff --git a/r2/r2/lib/db/tdb_sql.py b/r2/r2/lib/db/tdb_sql.py index dcbb97b6f..2b789664f 100644 --- a/r2/r2/lib/db/tdb_sql.py +++ b/r2/r2/lib/db/tdb_sql.py @@ -273,7 +273,10 @@ def get_rel_type_id(name): return rel_types_name[name][0] def get_write_table(tables): - return tables[0] + if g.disallow_db_writes: + raise Exception("not so fast! writes are not allowed on this app.") + else: + return tables[0] def get_read_table(tables): #shortcut with 1 entry @@ -320,14 +323,15 @@ def get_read_table(tables): #add in the over-connected machines with a 1% weight ip_weights.extend((ip, .01) for ip in no_connections) - #rebalance the weights - total_weight = sum(w[1] for w in ip_weights) - ip_weights = [(ip, weight / total_weight) - for ip, weight in ip_weights] + #rebalance the weights + total_weight = sum(w[1] for w in ip_weights) + ip_weights = [(ip, weight / total_weight) + for ip, weight in ip_weights] r = random.random() for ip, load in ip_weights: if r < load: + # print "db ip: %s" % str(ips[ip][0].metadata.bind.url.host) return ips[ip] else: r = r - load diff --git a/r2/r2/lib/db/thing.py b/r2/r2/lib/db/thing.py index 290f00ce9..bdb455e88 100644 --- a/r2/r2/lib/db/thing.py +++ b/r2/r2/lib/db/thing.py @@ -103,14 +103,13 @@ class DataThing(object): else: old_val = self._t.get(attr, self._defaults.get(attr)) self._t[attr] = val - if make_dirty and val != old_val: self._dirties[attr] = (old_val, val) def __getattr__(self, attr): #makes pickling work for some reason if attr.startswith('__'): - raise AttributeError + raise AttributeError, attr try: if hasattr(self, '_t'): @@ -302,6 +301,23 @@ class DataThing(object): else: return filter(None, (bases.get(i) for i in ids)) + @classmethod + def _byID36(cls, id36s, return_dict = True, **kw): + + id36s, single = tup(id36s, True) + + # will fail if it's not a string + ids = [ int(x, 36) for x in id36s ] + + things = cls._byID(ids, return_dict=True, **kw) + + if single: + return things.values()[0] + elif return_dict: + return things + else: + return things.values() + @classmethod def _by_fullname(cls, names, return_dict = True, @@ -454,6 +470,10 @@ class Thing(DataThing): def _controversy(self): return sorts.controversy(self._ups, self._downs) + @property + def _confidence(self): + return sorts.confidence(self._ups, self._downs) + @classmethod def _build(cls, id, bases): return cls(bases.ups, bases.downs, bases.date, diff --git a/r2/r2/lib/emailer.py b/r2/r2/lib/emailer.py index bbf309164..43199bbcb 100644 --- a/r2/r2/lib/emailer.py +++ b/r2/r2/lib/emailer.py @@ -21,51 +21,61 @@ ################################################################################ from email.MIMEText import MIMEText from pylons.i18n import _ -from pylons import c, g, request -from r2.lib.pages import PasswordReset, Share, Mail_Opt -from r2.lib.utils import timeago -from r2.models import passhash, Email, Default, has_opted_out -from r2.config import cache +from pylons import c, g +from r2.lib.utils import timeago, query_string +from r2.models import passhash, Email, Default, has_opted_out, Account import os, random, datetime -import smtplib, traceback, sys - -def email_address(name, address): - return '"%s" <%s>' % (name, address) if name else address -feedback = email_address('reddit feedback', g.feedback_email) - -def send_mail(msg, fr, to): - session = smtplib.SMTP(g.smtp_server) - session.sendmail(fr, to, msg.as_string()) - session.quit() - -def simple_email(to, fr, subj, body): - def utf8(s): - return s.encode('utf8') if isinstance(s, unicode) else s - msg = MIMEText(utf8(body)) - msg.set_charset('utf8') - msg['To'] = utf8(to) - msg['From'] = utf8(fr) - msg['Subject'] = utf8(subj) - send_mail(msg, fr, to) - -def password_email(user): - key = passhash(random.randint(0, 1000), user.email) - passlink = 'http://' + g.domain + '/resetpassword/' + key - print "Generated password reset link: " + passlink - cache.set("reset_%s" %key, user._id, time=1800) - simple_email(user.email, 'reddit@reddit.com', - 'reddit.com password reset', - PasswordReset(user=user, passlink=passlink).render(style='email')) - +import traceback, sys, smtplib def _feedback_email(email, body, kind, name='', reply_to = ''): """Function for handling feedback and ad_inq emails. Adds an email to the mail queue to the feedback email account.""" Email.handler.add_to_queue(c.user if c.user_is_loggedin else None, - None, [feedback], name, email, - datetime.datetime.now(), - request.ip, kind, body = body, - reply_to = reply_to) + g.feedback_email, name, email, + kind, body = body, reply_to = reply_to) + +def _system_email(email, body, kind, reply_to = "", thing = None): + """ + For sending email from the system to a user (reply address will be + feedback and the name will be reddit.com) + """ + Email.handler.add_to_queue(c.user if c.user_is_loggedin else None, + email, g.domain, g.feedback_email, + kind, body = body, reply_to = reply_to, + thing = thing) + +def verify_email(user, dest): + """ + For verifying an email address + """ + from r2.lib.pages import VerifyEmail + key = passhash(user.name, user.email) + user.email_verified = False + user._commit() + emaillink = ('http://' + g.domain + '/verification/' + key + + query_string(dict(dest=dest))) + print "Generated email verification link: " + emaillink + g.cache.set("email_verify_%s" %key, user._id, time=1800) + + _system_email(user.email, + VerifyEmail(user=user, + emaillink = emaillink).render(style='email'), + Email.Kind.VERIFY_EMAIL) + +def password_email(user): + """ + For reseting a user's password. + """ + from r2.lib.pages import PasswordReset + key = passhash(random.randint(0, 1000), user.email) + passlink = 'http://' + g.domain + '/resetpassword/' + key + print "Generated password reset link: " + passlink + g.cache.set("reset_%s" %key, user._id, time=1800) + _system_email(user.email, + PasswordReset(user=user, + passlink=passlink).render(style='email'), + Email.Kind.RESET_PASSWORD) + def feedback_email(email, body, name='', reply_to = ''): """Queues a feedback email to the feedback account.""" @@ -77,41 +87,49 @@ def ad_inq_email(email, body, name='', reply_to = ''): return _feedback_email(email, body, Email.Kind.ADVERTISE, name = name, reply_to = reply_to) - +def i18n_email(email, body, name='', reply_to = ''): + """Queues a ad_inq email to the feedback account.""" + return _feedback_email(email, body, Email.Kind.HELP_TRANSLATE, name = name, + reply_to = reply_to) + def share(link, emails, from_name = "", reply_to = "", body = ""): """Queues a 'share link' email.""" now = datetime.datetime.now(g.tz) ival = now - timeago(g.new_link_share_delay) date = max(now,link._date + ival) - Email.handler.add_to_queue(c.user, link, emails, from_name, g.share_reply, - date, request.ip, Email.Kind.SHARE, - body = body, reply_to = reply_to) - -def send_queued_mail(): + Email.handler.add_to_queue(c.user, emails, from_name, g.share_reply, + Email.Kind.SHARE, date = date, + body = body, reply_to = reply_to, + thing = link) + +def send_queued_mail(test = False): """sends mail from the mail queue to smtplib for delivery. Also, on successes, empties the mail queue and adds all emails to the sent_mail list.""" + from r2.lib.pages import PasswordReset, Share, Mail_Opt, VerifyEmail, Promo_Email now = datetime.datetime.now(g.tz) if not c.site: c.site = Default clear = False - session = smtplib.SMTP(g.smtp_server) - # convienence funciton for sending the mail to the singly-defined session and - # marking the mail as read. + if not test: + session = smtplib.SMTP(g.smtp_server) def sendmail(email): try: - session.sendmail(email.fr_addr, email.to_addr, - email.to_MIMEText().as_string()) - email.set_sent(rejected = False) + if test: + print email.to_MIMEText().as_string() + else: + session.sendmail(email.fr_addr, email.to_addr, + email.to_MIMEText().as_string()) + email.set_sent(rejected = False) # exception happens only for local recipient that doesn't exist except (smtplib.SMTPRecipientsRefused, smtplib.SMTPSenderRefused, UnicodeDecodeError): # handle error and print, but don't stall the rest of the queue - print "Handled error sending mail (traceback to follow)" - traceback.print_exc(file = sys.stdout) + print "Handled error sending mail (traceback to follow)" + traceback.print_exc(file = sys.stdout) email.set_sent(rejected = True) - + try: for email in Email.get_unsent(now): @@ -124,38 +142,32 @@ def send_queued_mail(): msg_hash = email.msg_hash, link = email.thing, body = email.body).render(style = "email") - try: - email.subject = _("[reddit] %(user)s has shared a link with you") % \ - {"user": email.from_name()} - except UnicodeDecodeError: - email.subject = _("[reddit] a user has shared a link with you") - sendmail(email) elif email.kind == Email.Kind.OPTOUT: email.body = Mail_Opt(msg_hash = email.msg_hash, leave = True).render(style = "email") - email.subject = _("[reddit] email removal notice") - sendmail(email) - elif email.kind == Email.Kind.OPTIN: email.body = Mail_Opt(msg_hash = email.msg_hash, leave = False).render(style = "email") - email.subject = _("[reddit] email addition notice") - sendmail(email) + elif email.kind in (Email.Kind.ACCEPT_PROMO, + Email.Kind.REJECT_PROMO, + Email.Kind.QUEUED_PROMO, + Email.Kind.LIVE_PROMO, + Email.Kind.BID_PROMO, + Email.Kind.FINISHED_PROMO, + Email.Kind.NEW_PROMO): + email.body = Promo_Email(link = email.thing, + kind = email.kind, + body = email.body).render(style="email") - elif email.kind in (Email.Kind.FEEDBACK, Email.Kind.ADVERTISE): - if email.kind == Email.Kind.FEEDBACK: - email.subject = "[feedback] feedback from '%s'" % \ - email.from_name() - else: - email.subject = "[ad_inq] feedback from '%s'" % \ - email.from_name() - sendmail(email) - # handle failure - else: + # handle unknown types here + elif email.kind not in Email.Kind: email.set_sent(rejected = True) + continue + sendmail(email) finally: - session.quit() + if not test: + session.quit() # clear is true if anything was found and processed above if clear: @@ -168,9 +180,7 @@ def opt_out(msg_hash): address has been opted out of receiving any future mail)""" email, added = Email.handler.opt_out(msg_hash) if email and added: - Email.handler.add_to_queue(None, None, [email], "reddit.com", - datetime.datetime.now(g.tz), - '127.0.0.1', Email.Kind.OPTOUT) + _system_email(email, "", Email.Kind.OPTOUT) return email, added def opt_in(msg_hash): @@ -178,7 +188,33 @@ def opt_in(msg_hash): from our opt out list)""" email, removed = Email.handler.opt_in(msg_hash) if email and removed: - Email.handler.add_to_queue(None, None, [email], "reddit.com", - datetime.datetime.now(g.tz), - '127.0.0.1', Email.Kind.OPTIN) + _system_email(email, "", Email.Kind.OPTIN) return email, removed + + +def _promo_email(thing, kind, body = ""): + a = Account._byID(thing.author_id) + return _system_email(a.email, body, kind, thing = thing, + reply_to = "selfservicesupport@reddit.com") + + +def new_promo(thing): + return _promo_email(thing, Email.Kind.NEW_PROMO) + +def promo_bid(thing): + return _promo_email(thing, Email.Kind.BID_PROMO) + +def accept_promo(thing): + return _promo_email(thing, Email.Kind.ACCEPT_PROMO) + +def reject_promo(thing, reason = ""): + return _promo_email(thing, Email.Kind.REJECT_PROMO, reason) + +def queue_promo(thing): + return _promo_email(thing, Email.Kind.QUEUED_PROMO) + +def live_promo(thing): + return _promo_email(thing, Email.Kind.LIVE_PROMO) + +def finished_promo(thing): + return _promo_email(thing, Email.Kind.FINISHED_PROMO) diff --git a/r2/r2/lib/filters.py b/r2/r2/lib/filters.py index 19eebc171..77b6672b3 100644 --- a/r2/r2/lib/filters.py +++ b/r2/r2/lib/filters.py @@ -127,7 +127,6 @@ code_re = re.compile('([^<]+)') a_re = re.compile('>([^<]+)') fix_url = re.compile('<(http://[^\s\'\"\]\)]+)>') - #TODO markdown should be looked up in batch? #@memoize('markdown') def safemarkdown(text, nofollow=False, target=None): diff --git a/r2/r2/lib/jsonresponse.py b/r2/r2/lib/jsonresponse.py index 5b60db4d7..5fcf12dff 100644 --- a/r2/r2/lib/jsonresponse.py +++ b/r2/r2/lib/jsonresponse.py @@ -78,10 +78,12 @@ class JsonResponse(object): def has_errors(self, field_name, *errors, **kw): have_error = False + field_name = tup(field_name) for error_name in errors: - if (error_name, field_name) in c.errors: - self.set_error(error_name, field_name) - have_error = True + for fname in field_name: + if (error_name, fname) in c.errors: + self.set_error(error_name, fname) + have_error = True return have_error def _things(self, things, action, *a, **kw): diff --git a/r2/r2/lib/jsontemplates.py b/r2/r2/lib/jsontemplates.py index 142b850eb..dc9ab563b 100644 --- a/r2/r2/lib/jsontemplates.py +++ b/r2/r2/lib/jsontemplates.py @@ -74,6 +74,10 @@ class JsonTemplate(Template): def render(self, thing = None, *a, **kw): return ObjectTemplate({}) +class TakedownJsonTemplate(JsonTemplate): + def render(self, thing = None, *a, **kw): + return thing.explanation + class TableRowTemplate(JsonTemplate): def cells(self, thing): raise NotImplementedError @@ -202,11 +206,12 @@ class LinkJsonTemplate(ThingJsonTemplate): domain = "domain", title = "title", url = "url", - author = "author", + author = "author", thumbnail = "thumbnail", media = "media_object", media_embed = "media_embed", selftext = "selftext", + selftext_html= "selftext_html", num_comments = "num_comments", subreddit = "subreddit", subreddit_id = "subreddit_id") @@ -228,6 +233,8 @@ class LinkJsonTemplate(ThingJsonTemplate): elif attr == 'subreddit_id': return thing.subreddit._fullname elif attr == 'selftext': + return thing.selftext + elif attr == 'selftext_html': return safemarkdown(thing.selftext) return ThingJsonTemplate.thing_attr(self, thing, attr) @@ -237,6 +244,9 @@ class LinkJsonTemplate(ThingJsonTemplate): return d +class PromotedLinkJsonTemplate(LinkJsonTemplate): + _data_attrs_ = LinkJsonTemplate.data_attrs(promoted = "promoted") + del _data_attrs_['author'] class CommentJsonTemplate(ThingJsonTemplate): _data_attrs_ = ThingJsonTemplate.data_attrs(ups = "upvotes", @@ -293,8 +303,13 @@ class MoreCommentJsonTemplate(CommentJsonTemplate): def kind(self, wrapped): return "more" + def thing_attr(self, thing, attr): + if attr in ('body', 'body_html'): + return "" + return CommentJsonTemplate.thing_attr(self, thing, attr) + def rendered_data(self, wrapped): - return ThingJsonTemplate.rendered_data(self, wrapped) + return CommentJsonTemplate.rendered_data(self, wrapped) class MessageJsonTemplate(ThingJsonTemplate): _data_attrs_ = ThingJsonTemplate.data_attrs(new = "new", @@ -309,9 +324,9 @@ class MessageJsonTemplate(ThingJsonTemplate): def thing_attr(self, thing, attr): if attr == "was_comment": - return hasattr(thing, "was_comment") + return thing.was_comment elif attr == "context": - return ("" if not hasattr(thing, "was_comment") + return ("" if not thing.was_comment else thing.permalink + "?context=3") elif attr == "dest": return thing.to.name diff --git a/r2/r2/lib/media.py b/r2/r2/lib/media.py index e88dad847..1d31f8306 100644 --- a/r2/r2/lib/media.py +++ b/r2/r2/lib/media.py @@ -23,17 +23,17 @@ from pylons import g, config from r2.models.link import Link -from r2.lib.workqueue import WorkQueue from r2.lib import s3cp from r2.lib.utils import timeago, fetch_things2 +from r2.lib.utils import TimeoutFunction, TimeoutFunctionException from r2.lib.db.operators import desc from r2.lib.scraper import make_scraper, str_to_image, image_to_str, prepare_image +from r2.lib import amqp import tempfile -from Queue import Queue +import traceback s3_thumbnail_bucket = g.s3_thumb_bucket -media_period = g.media_period threads = 20 log = g.log @@ -41,6 +41,7 @@ def thumbnail_url(link): """Given a link, returns the url for its thumbnail based on its fullname""" return 'http:/%s%s.png' % (s3_thumbnail_bucket, link._fullname) + def upload_thumb(link, image): """Given a link and an image, uploads the image to s3 into an image based on the link's fullname""" @@ -52,26 +53,6 @@ def upload_thumb(link, image): s3cp.send_file(f.name, resource, 'image/png', 'public-read', None, False) log.debug('thumbnail %s: %s' % (link._fullname, thumbnail_url(link))) -def make_link_info_job(results, link, useragent): - """Returns a unit of work to send to a work queue that downloads a - link's thumbnail and media object. Places the result in the results - dict""" - def job(): - try: - scraper = make_scraper(link.url) - - thumbnail = scraper.thumbnail() - media_object = scraper.media_object() - - if thumbnail: - upload_thumb(link, thumbnail) - - results[link] = (thumbnail, media_object) - except: - log.warning('error fetching %s %s' % (link._fullname, link.url)) - raise - - return job def update_link(link, thumbnail, media_object): """Sets the link's has_thumbnail and media_object attributes iin the @@ -84,40 +65,48 @@ def update_link(link, thumbnail, media_object): link._commit() -def process_new_links(period = media_period, force = False): - """Fetches links from the last period and sets their media - properities. If force is True, it will fetch properities for links - even if the properties already exist""" - links = Link._query(Link.c._date > timeago(period), sort = desc('_date'), - data = True) - results = {} - jobs = [] - for link in fetch_things2(links): - if link.is_self or link.promoted: - continue - elif not force and (link.has_thumbnail or link.media_object): - continue - jobs.append(make_link_info_job(results, link, g.useragent)) +def set_media(link, force = False): + if link.is_self: + return + if not force and link.promoted: + return + elif not force and (link.has_thumbnail or link.media_object): + return + + scraper = make_scraper(link.url) - #send links to a queue - wq = WorkQueue(jobs, num_workers = 20, timeout = 30) - wq.start() - wq.jobs.join() + thumbnail = scraper.thumbnail() + media_object = scraper.media_object() - #when the queue is finished, do the db writes in this thread - for link, info in results.items(): - update_link(link, info[0], info[1]) + if thumbnail: + upload_thumb(link, thumbnail) -def set_media(link): - """Sets the media properties for a single link.""" - results = {} - make_link_info_job(results, link, g.useragent)() - update_link(link, *results[link]) + update_link(link, thumbnail, media_object) def force_thumbnail(link, image_data): image = str_to_image(image_data) image = prepare_image(image) upload_thumb(link, image) update_link(link, thumbnail = True, media_object = None) - + +def run(): + def process_msgs(msgs): + def _process_link(fname): + print "media: Processing %s" % fname + + link = Link._by_fullname(fname, data=True, return_dict=False) + set_media(link) + for msg in msgs: + fname = msg.body + try: + TimeoutFunction(_process_link, 30)(fname) + except TimeoutFunctionException: + print "Timed out on %s" % fname + except KeyboardInterrupt: + raise + except: + print "Error fetching %s" % fname + print traceback.format_exc() + + amqp.handle_items('scraper_q', process_msgs, limit=1) diff --git a/r2/r2/lib/menus.py b/r2/r2/lib/menus.py index 06408bfbb..9d2de0052 100644 --- a/r2/r2/lib/menus.py +++ b/r2/r2/lib/menus.py @@ -1,4 +1,3 @@ - # The contents of this file are subject to the Common Public Attribution # License Version 1.0. (the "License"); you may not use this file except in # compliance with the License. You may obtain a copy of the License at @@ -62,6 +61,7 @@ menu = MenuHandler(hot = _('hot'), more = _('more'), relevance = _('relevance'), controversial = _('controversial'), + confidence = _('best'), saved = _('saved {toolbar}'), recommended = _('recommended'), rising = _('rising'), @@ -83,7 +83,6 @@ menu = MenuHandler(hot = _('hot'), adminon = _("turn admin on"), adminoff = _("turn admin off"), prefs = _("preferences"), - stats = _("stats"), submit = _("submit"), help = _("help"), blog = _("the reddit blog"), @@ -115,27 +114,31 @@ menu = MenuHandler(hot = _('hot'), sent = _("sent"), # comments + comments = _("comments {toolbar}"), related = _("related"), details = _("details"), duplicates = _("other discussions (%(num)s)"), shirt = _("shirt"), - traffic = _("traffic"), + traffic = _("traffic stats"), # reddits home = _("home"), about = _("about"), - edit = _("edit"), - banned = _("banned"), + edit = _("edit this reddit"), + moderators = _("edit moderators"), + contributors = _("edit contributors"), + banned = _("ban users"), banusers = _("ban users"), popular = _("popular"), create = _("create"), mine = _("my reddits"), - i18n = _("translate site"), + i18n = _("help translate"), + awards = _("awards"), promoted = _("promoted"), reporters = _("reporters"), - reports = _("reports"), + reports = _("reported links"), reportedauth = _("reported authors"), info = _("info"), share = _("share"), @@ -148,9 +151,15 @@ menu = MenuHandler(hot = _('hot'), deleted = _("deleted"), reported = _("reported"), - promote = _('promote'), - new_promo = _('new promoted link'), - current_promos = _('promoted links'), + promote = _('self-serve'), + new_promo = _('create promotion'), + my_current_promos = _('my promoted links'), + current_promos = _('all promoted links'), + future_promos = _('unapproved'), + graph = _('analytics'), + live_promos = _('live'), + unpaid_promos = _('unpaid'), + pending_promos = _('pending') ) def menu_style(type): @@ -179,7 +188,10 @@ class NavMenu(Styled): base_path = '', separator = '|', **kw): self.options = options self.base_path = base_path - kw['style'], kw['css_class'] = menu_style(type) + + #add the menu style, but preserve existing css_class parameter + kw['style'], css_class = menu_style(type) + kw['css_class'] = css_class + ' ' + kw.get('css_class', '') #used by flatlist to delimit menu items self.separator = separator @@ -223,11 +235,11 @@ class NavButton(Styled): nocname=False, opt = '', aliases = [], target = "", style = "plain", **kw): # keep original dest to check against c.location when rendering - aliases = set(a.rstrip('/') for a in aliases) - aliases.add(dest.rstrip('/')) + aliases = set(_force_unicode(a.rstrip('/')) for a in aliases) + aliases.add(_force_unicode(dest.rstrip('/'))) self.request_params = dict(request.GET) - self.stripped_path = request.path.rstrip('/').lower() + self.stripped_path = _force_unicode(request.path.rstrip('/').lower()) Styled.__init__(self, style = style, sr_path = sr_path, nocname = nocname, target = target, @@ -264,6 +276,8 @@ class NavButton(Styled): else: if self.stripped_path == self.bare_path: return True + if self.bare_path and self.stripped_path.startswith(self.bare_path): + return True if self.stripped_path in self.aliases: return True @@ -388,10 +402,13 @@ class SortMenu(SimpleGetMenu): return operators.desc('_score') elif sort == 'controversial': return operators.desc('_controversy') + elif sort == 'confidence': + return operators.desc('_confidence') class CommentSortMenu(SortMenu): """Sort menu for comments pages""" - options = ('hot', 'new', 'controversial', 'top', 'old') + default = 'confidence' + options = ('hot', 'new', 'controversial', 'top', 'old', 'confidence') class SearchSortMenu(SortMenu): """Sort menu for search pages.""" diff --git a/r2/r2/lib/migrate.py b/r2/r2/lib/migrate.py index 18fec62cc..53a59b6f2 100644 --- a/r2/r2/lib/migrate.py +++ b/r2/r2/lib/migrate.py @@ -22,6 +22,7 @@ """ One-time use functions to migrate from one reddit-version to another """ +from r2.lib.promote import * def add_allow_top_to_srs(): "Add the allow_top property to all stored subreddits" @@ -33,3 +34,104 @@ def add_allow_top_to_srs(): sort = desc('_date')) for sr in fetch_things2(q): sr.allow_top = True; sr._commit() + +def convert_promoted(): + """ + should only need to be run once to update old style promoted links + to the new style. + """ + from r2.lib.utils import fetch_things2 + from r2.lib import authorize + + q = Link._query(Link.c.promoted == (True, False), + sort = desc("_date")) + sr_id = PromoteSR._id + bid = 100 + with g.make_lock(promoted_lock_key): + promoted = {} + set_promoted({}) + for l in fetch_things2(q): + print "updating:", l + try: + if not l._loaded: l._load() + # move the promotion into the promo subreddit + l.sr_id = sr_id + # set it to accepted (since some of the update functions + # check that it is not already promoted) + l.promote_status = STATUS.accepted + author = Account._byID(l.author_id) + l.promote_trans_id = authorize.auth_transaction(bid, author, -1, l) + l.promote_bid = bid + l.maximum_clicks = None + l.maximum_views = None + # set the dates + start = getattr(l, "promoted_on", l._date) + until = getattr(l, "promote_until", None) or \ + (l._date + timedelta(1)) + l.promote_until = None + update_promo_dates(l, start, until) + # mark it as promoted if it was promoted when we got there + if l.promoted and l.promote_until > datetime.now(g.tz): + l.promote_status = STATUS.pending + else: + l.promote_status = STATUS.finished + + if not hasattr(l, "disable_comments"): + l.disable_comments = False + # add it to the auction list + if l.promote_status == STATUS.pending and l._fullname not in promoted: + promoted[l._fullname] = auction_weight(l) + l._commit() + except AttributeError: + print "BAD THING:", l + print promoted + set_promoted(promoted) + # run what is normally in a cron job to clear out finished promos + #promote_promoted() + +def store_market(): + + """ + create index ix_promote_date_actual_end on promote_date(actual_end); + create index ix_promote_date_actual_start on promote_date(actual_start); + create index ix_promote_date_start_date on promote_date(start_date); + create index ix_promote_date_end_date on promote_date(end_date); + + alter table promote_date add column account_id bigint; + create index ix_promote_date_account_id on promote_date(account_id); + alter table promote_date add column bid real; + alter table promote_date add column refund real; + + """ + + for p in PromoteDates.query().all(): + l = Link._by_fullname(p.thing_name, True) + if hasattr(l, "promote_bid") and hasattr(l, "author_id"): + p.account_id = l.author_id + p._commit() + PromoteDates.update(l, l._date, l.promote_until) + PromoteDates.update_bid(l) + +def subscribe_to_blog_and_annoucements(filename): + import re + from time import sleep + from r2.models import Account, Subreddit + + r_blog = Subreddit._by_name("blog") + r_announcements = Subreddit._by_name("announcements") + + contents = file(filename).read() + numbers = [ int(s) for s in re.findall("\d+", contents) ] + +# d = Account._byID(numbers, data=True) + +# for i, account in enumerate(d.values()): + for i, account_id in enumerate(numbers): + account = Account._byID(account_id, data=True) + + for sr in r_blog, r_announcements: + if sr.add_subscriber(account): + sr._incr("_ups", 1) + print ("%d: subscribed %s to %s" % (i, account.name, sr.name)) + else: + print ("%d: didn't subscribe %s to %s" % (i, account.name, sr.name)) diff --git a/r2/r2/lib/normalized_hot.py b/r2/r2/lib/normalized_hot.py index ed86a00d7..699107bf8 100644 --- a/r2/r2/lib/normalized_hot.py +++ b/r2/r2/lib/normalized_hot.py @@ -101,7 +101,7 @@ def normalized_hot_cached(sr_ids): if not items: continue - top_score = max(items[0]._hot, 1) + top_score = max(max(x._hot for x in items), 1) if items: results.extend((l, l._hot / top_score) for l in items) diff --git a/r2/r2/lib/organic.py b/r2/r2/lib/organic.py index 8ff386d85..da011bbf0 100644 --- a/r2/r2/lib/organic.py +++ b/r2/r2/lib/organic.py @@ -24,7 +24,7 @@ from r2.lib.memoize import memoize from r2.lib.normalized_hot import get_hot, only_recent from r2.lib import count from r2.lib.utils import UniqueIterator, timeago -from r2.lib.promote import get_promoted +from r2.lib.promote import random_promoted from pylons import c @@ -45,36 +45,28 @@ def insert_promoted(link_names, sr_ids, logged_in): Inserts promoted links into an existing organic list. Destructive on `link_names' """ - promoted_items = get_promoted() + promoted_items = random_promoted() if not promoted_items: return - def my_keepfn(l): - if l.promoted_subscribersonly and l.sr_id not in sr_ids: - return False - else: - return keep_link(l) - # no point in running the builder over more promoted links than # we'll even use max_promoted = max(1,len(link_names)/promoted_every_n) - # in the future, we may want to weight this sorting somehow - random.shuffle(promoted_items) - # remove any that the user has acted on - builder = IDBuilder(promoted_items, - skip = True, keep_fn = my_keepfn, - num = max_promoted) + def keep(item): + if c.user_is_loggedin and c.user._id == item.author_id: + return True + else: + return item.keep_item(item) + + builder = IDBuilder(promoted_items, keep_fn = keep, + skip = True, num = max_promoted) promoted_items = builder.get_items()[0] if not promoted_items: return - - #make a copy before we start messing with things - orig_promoted = list(promoted_items) - # don't insert one at the head of the list 50% of the time for # logged in users, and 50% of the time for logged-off users when # the pool of promoted links is less than 3 (to avoid showing the @@ -82,11 +74,6 @@ def insert_promoted(link_names, sr_ids, logged_in): if (logged_in or len(promoted_items) < 3) and random.choice((True,False)): promoted_items.insert(0, None) - #repeat the same promoted links for non logged in users - if not logged_in: - while len(promoted_items) * promoted_every_n < len(link_names): - promoted_items.extend(orig_promoted) - # insert one promoted item for every N items for i, item in enumerate(promoted_items): pos = i * promoted_every_n + i @@ -106,11 +93,9 @@ def cached_organic_links(user_id, langs): sr_ids = Subreddit.user_subreddits(user) sr_count = count.get_link_counts() - #only use links from reddits that you're subscribed to link_names = filter(lambda n: sr_count[n][1] in sr_ids, sr_count.keys()) link_names.sort(key = lambda n: sr_count[n][0]) - #potentially add a up and coming link if random.choice((True, False)) and sr_ids: sr = Subreddit._byID(random.choice(sr_ids)) @@ -122,6 +107,8 @@ def cached_organic_links(user_id, langs): new_item = random.choice(items[1:4]) link_names.insert(0, new_item._fullname) + insert_promoted(link_names, sr_ids, user_id is not None) + # remove any that the user has acted on builder = IDBuilder(link_names, skip = True, keep_fn = keep_link, @@ -133,8 +120,6 @@ def cached_organic_links(user_id, langs): if user_id: update_pos(0) - insert_promoted(link_names, sr_ids, user_id is not None) - # remove any duplicates caused by insert_promoted if the user is logged in if user_id: link_names = list(UniqueIterator(link_names)) diff --git a/r2/r2/lib/pages/admin_pages.py b/r2/r2/lib/pages/admin_pages.py index 6d0c6e396..8e05bf987 100644 --- a/r2/r2/lib/pages/admin_pages.py +++ b/r2/r2/lib/pages/admin_pages.py @@ -26,6 +26,7 @@ from r2.lib.menus import NamedButton, NavButton, menu, NavMenu class AdminSidebar(Templated): def __init__(self, user): + Templated.__init__(self) self.user = user @@ -46,7 +47,9 @@ class AdminPage(Reddit): buttons = [] if g.translator: - buttons.append(NavButton(menu.i18n, "")) + buttons.append(NavButton(menu.i18n, "i18n")) + + buttons.append(NavButton(menu.awards, "awards")) admin_menu = NavMenu(buttons, title='show', base_path = '/admin', type="lightdrop") diff --git a/r2/r2/lib/pages/graph.py b/r2/r2/lib/pages/graph.py index a3184aa25..376002bc1 100644 --- a/r2/r2/lib/pages/graph.py +++ b/r2/r2/lib/pages/graph.py @@ -138,9 +138,9 @@ class LineGraph(object): def __init__(self, xydata, colors = ("FF4500", "336699"), width = 300, height = 175): - + series = zip(*xydata) - + self.xdata = DataSeries(series[0]) self.ydata = map(DataSeries, series[1:]) self.width = width @@ -150,13 +150,13 @@ class LineGraph(object): def google_chart(self, multiy = True, ylabels = [], title = "", bar_fmt = True): xdata, ydata = self.xdata, self.ydata - + # Bar format makes the line chart look like it is a series of # contiguous bars without the boundary line between each bar. if bar_fmt: xdata = DataSeries(range(len(self.xdata))).toBarX() ydata = [y.toBarY() for y in self.ydata] - + # TODO: currently we are only supporting time series. Make general xaxis = make_date_axis_labels(self.xdata) diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index f78088428..3ce70866a 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -23,29 +23,31 @@ from r2.lib.wrapped import Wrapped, Templated, NoTemplateFound, CachedTemplate from r2.models import Account, Default from r2.models import FakeSubreddit, Subreddit from r2.models import Friends, All, Sub, NotFound, DomainSR -from r2.models import Link, Printable +from r2.models import Link, Printable, Trophy, bidding, PromoteDates from r2.config import cache +from r2.lib.tracking import AdframeInfo from r2.lib.jsonresponse import json_respond from r2.lib.jsontemplates import is_api from pylons.i18n import _, ungettext from pylons import c, request, g from pylons.controllers.util import abort +from r2.lib import promote from r2.lib.traffic import load_traffic, load_summary from r2.lib.captcha import get_iden -from r2.lib.filters import spaceCompress, _force_unicode, _force_utf8, unsafe +from r2.lib.filters import spaceCompress, _force_unicode, _force_utf8, unsafe, websafe from r2.lib.menus import NavButton, NamedButton, NavMenu, PageNameNav, JsButton from r2.lib.menus import SubredditButton, SubredditMenu from r2.lib.menus import OffsiteButton, menu, JsNavMenu from r2.lib.strings import plurals, rand_strings, strings, Score from r2.lib.utils import title_to_url, query_string, UrlParser, to_js, vote_hash -from r2.lib.utils import link_duplicates +from r2.lib.utils import link_duplicates, make_offset_date, to_csv from r2.lib.template_helpers import add_sr, get_domain from r2.lib.subreddit_search import popular_searches from r2.lib.scraper import scrapers import sys, random, datetime, locale, calendar, simplejson, re -import graph +import graph, pycountry from itertools import chain from urllib import quote @@ -128,6 +130,27 @@ class Reddit(Templated): self.toolbars = self.build_toolbars() + def sr_admin_menu(self): + buttons = [NamedButton('edit', css_class = 'reddit-edit'), + NamedButton('moderators', css_class = 'reddit-moderators')] + + if c.site.type != 'public': + buttons.append(NamedButton('contributors', + css_class = 'reddit-contributors')) + + buttons.extend([ + NamedButton('traffic', css_class = 'reddit-traffic'), + NamedButton('reports', css_class = 'reddit-reported'), + NamedButton('spam', css_class = 'reddit-spam'), + NamedButton('banned', css_class = 'reddit-ban'), + ]) + return [NavMenu(buttons, type = "flat_vert", base_path = "/about/", + css_class = "icon-menu", separator = '')] + + def sr_moderators(self): + accounts = [Account._byID(uid, True) for uid in c.site.moderators] + return [WrappedUser(a) for a in accounts if not a._deleted] + def rightbox(self): """generates content in
""" @@ -142,6 +165,16 @@ class Reddit(Templated): #don't show the subreddit info bar on cnames if not isinstance(c.site, FakeSubreddit) and not c.cname: ps.append(SubredditInfoBar()) + ps.append(SponsorshipBox()) + + moderators = self.sr_moderators() + if moderators: + ps.append(SideContentBox(_('moderators'), moderators)) + + if (c.user_is_loggedin and + (c.site.is_moderator(c.user) or c.user_is_admin)): + ps.append(SideContentBox(_('admin box'), self.sr_admin_menu())) + if self.submit_box: ps.append(SideBox(_('Submit a link'), @@ -223,10 +256,10 @@ class Reddit(Templated): if c.user_is_loggedin: if c.user_is_admin: - more_buttons.append(NamedButton('admin')) - - if c.user_is_sponsor: - more_buttons.append(NamedButton('promote')) + more_buttons.append(NamedButton('admin', False)) + more_buttons.append(NamedButton('traffic', False)) + if c.user.pref_show_promote or c.user_is_sponsor: + more_buttons.append(NavButton(menu.promote, 'promoted', False)) #if there's only one button in the dropdown, get rid of the dropdown if len(more_buttons) == 1: @@ -236,12 +269,12 @@ class Reddit(Templated): toolbar = [NavMenu(main_buttons, type='tabmenu')] if more_buttons: toolbar.append(NavMenu(more_buttons, title=menu.more, type='tabdrop')) - + if c.site != Default and not c.cname: toolbar.insert(0, PageNameNav('subreddit')) return toolbar - + def __repr__(self): return "" @@ -260,28 +293,31 @@ class RedditHeader(Templated): class RedditFooter(CachedTemplate): def cachable_attrs(self): - return [('path', request.path)] - - def nav(self): - return [NavMenu([NamedButton("toplinks", False), + return [('path', request.path), + ('buttons', [[(x.title, x.path) for x in y] for y in self.nav])] + + def __init__(self): + self.nav = [NavMenu([NamedButton("toplinks", False), NamedButton("mobile", False, nocname=True), OffsiteButton("rss", dest = '/.rss'), NamedButton("store", False, nocname=True), - NamedButton("stats", False, nocname=True), + NamedButton("awards", False, nocname=True), NamedButton('random', False, nocname=False), - NamedButton("feedback", False),], + ], title = _('site links'), type = 'flat_vert', separator = ''), - - NavMenu([NamedButton("help", False, nocname=True), + + NavMenu([NamedButton("help", False, nocname=True), OffsiteButton(_("FAQ"), dest = '/help/faq', nocname=True), OffsiteButton(_("reddiquette"), nocname=True, - dest = '/help/reddiquette')], + dest = '/help/reddiquette'), + NamedButton("feedback", False), + NamedButton("i18n", False), + ], title = _('help'), type = 'flat_vert', separator = ''), - - NavMenu([NamedButton("bookmarklets", False), + NavMenu([NamedButton("bookmarklets", False), NamedButton("buttons", True), NamedButton("code", False, nocname=True), NamedButton("socialite", False), @@ -289,8 +325,7 @@ class RedditFooter(CachedTemplate): NamedButton("iphone", False),], title = _('reddit tools'), type = 'flat_vert', separator = ''), - - NavMenu([NamedButton("blog", False, nocname=True), + NavMenu([NamedButton("blog", False, nocname=True), NamedButton("ad_inq", False, nocname=True), OffsiteButton('reddit.tv', "http://www.reddit.tv"), OffsiteButton('redditall', "http://www.redditall.com"), @@ -298,7 +333,7 @@ class RedditFooter(CachedTemplate): "http://www.redditjobs.com")], title = _('about us'), type = 'flat_vert', separator = ''), - NavMenu([OffsiteButton('BaconBuzz', + NavMenu([OffsiteButton('BaconBuzz', "http://www.baconbuzz.com"), OffsiteButton('Destructoid reddit', "http://reddit.destructoid.com"), @@ -314,7 +349,7 @@ class RedditFooter(CachedTemplate): "http://www.idealistnews.com"),], title = _('brothers'), type = 'flat_vert', separator = ''), - NavMenu([OffsiteButton('Wired.com', + NavMenu([OffsiteButton('Wired.com', "http://www.wired.com"), OffsiteButton('Ars Technica', "http://www.arstechnica.com"), @@ -327,7 +362,7 @@ class RedditFooter(CachedTemplate): title = _('sisters'), type = 'flat_vert', separator = '') ] - + CachedTemplate.__init__(self) class ClickGadget(Templated): def __init__(self, links, *a, **kw): @@ -369,24 +404,13 @@ class SubredditInfoBar(CachedTemplate): def __init__(self, site = None): site = site or c.site - self.spam = site._spam - self.name = site.name - self.type = site.type - self.is_fake = isinstance(site, FakeSubreddit) - self.is_loggedin = c.user_is_loggedin - self.is_admin = c.user_is_admin - self.fullname = site._fullname - self.is_subscriber = bool(c.user_is_loggedin and \ - site.is_subscriber_defaults(c.user)) - self.is_moderator = bool(c.user_is_loggedin and \ - site.is_moderator(c.user)) - self.is_contributor = bool(site.type in ("private", "restricted") and \ - c.user_is_loggedin and \ - site.is_contributor(c.user)) - self.subscribers = site._ups - self.date = site._date - self.banner = getattr(site, "banner", None) + #hackity hack. do i need to add all the others props? + self.sr = list(wrap_links(site))[0] + + # we want to cache on the number of subscribers + self.subscribers = self.sr._ups + #so the menus cache properly self.path = request.path CachedTemplate.__init__(self) @@ -407,13 +431,21 @@ class SubredditInfoBar(CachedTemplate): return [NavMenu(buttons, type = "flat_vert", base_path = "/about/", separator = '')] +class SponsorshipBox(Templated): + pass + +class SideContentBox(Templated): + def __init__(self, title, content, helplink=None, extra_class=None): + Templated.__init__(self, title=title, helplink=helplink, + content=content, extra_class=extra_class) + class SideBox(CachedTemplate): """ Generic sidebox used to generate the 'submit' and 'create a reddit' boxes. """ def __init__(self, title, link, css_class='', subtitles = [], show_cover = False, nocname=False, sr_path = False): - Templated.__init__(self, link = link, target = '_top', + CachedTemplate.__init__(self, link = link, target = '_top', title = title, css_class = css_class, sr_path = sr_path, subtitles = subtitles, show_cover = show_cover, nocname=nocname) @@ -421,19 +453,22 @@ class SideBox(CachedTemplate): class PrefsPage(Reddit): """container for pages accessible via /prefs. No extension handling.""" - + extension_handling = False def __init__(self, show_sidebar = False, *a, **kw): Reddit.__init__(self, show_sidebar = show_sidebar, - title = "%s (%s)" %(_("preferences"), c.site.name.strip(' ')), + title = "%s (%s)" %(_("preferences"), + c.site.name.strip(' ')), *a, **kw) def build_toolbars(self): buttons = [NavButton(menu.options, ''), NamedButton('friends'), - NamedButton('update'), - NamedButton('delete')] + NamedButton('update')] + #if CustomerID.get_id(user): + # buttons += [NamedButton('payment')] + buttons += [NamedButton('delete')] return [PageNameNav('nomenu', title = _("preferences")), NavMenu(buttons, base_path = "/prefs", type="tabmenu")] @@ -444,7 +479,11 @@ class PrefOptions(Templated): class PrefUpdate(Templated): """Preference form for updating email address and passwords""" - pass + def __init__(self, email = True, password = True, verify = False): + self.email = email + self.password = password + self.verify = verify + Templated.__init__(self) class PrefDelete(Templated): """preference form for deleting a user's own account.""" @@ -551,6 +590,23 @@ class SearchPage(BoringPage): return self.content_stack((self.searchbar, self.infobar, self.nav_menu, self._content)) +class TakedownPage(BoringPage): + def __init__(self, link): + BoringPage.__init__(self, getattr(link, "takedown_title", _("bummer")), + content = TakedownPane(link)) + + def render(self, *a, **kw): + response = BoringPage.render(self, *a, **kw) + return response + + +class TakedownPane(Templated): + def __init__(self, link, *a, **kw): + self.link = link + self.explanation = getattr(self.link, "explanation", + _("this page is no longer available due to a copyright claim.")) + Templated.__init__(self, *a, **kw) + class CommentsPanel(Templated): """the side-panel on the reddit toolbar frame that shows the top comments of a link""" @@ -578,7 +634,10 @@ class LinkInfoPage(Reddit): def __init__(self, link = None, comment = None, link_title = '', subtitle = None, duplicates = None, *a, **kw): - wrapper = default_thing_wrapper(expand_children = True) + + expand_children = kw.get("expand_children", not bool(comment)) + + wrapper = default_thing_wrapper(expand_children=expand_children) # link_listing will be the one-element listing at the top self.link_listing = wrap_links(link, wrapper = wrapper) @@ -613,24 +672,31 @@ class LinkInfoPage(Reddit): def build_toolbars(self): base_path = "/%s/%s/" % (self.link._id36, title_to_url(self.link.title)) base_path = _force_utf8(base_path) + + def info_button(name, **fmt_args): return NamedButton(name, dest = '/%s%s' % (name, base_path), aliases = ['/%s/%s' % (name, self.link._id36)], fmt_args = fmt_args) + buttons = [] + if not getattr(self.link, "disable_comments", False): + buttons.extend([info_button('comments'), + info_button('related')]) - buttons = [info_button('comments'), - info_button('related')] + if not self.link.is_self and self.duplicates: + buttons.append(info_button('duplicates', + num = len(self.duplicates))) + if len(self.link.title) < 200 and g.spreadshirt_url: + buttons += [info_button('shirt')] - if not self.link.is_self and self.duplicates: - buttons.append(info_button('duplicates', num = len(self.duplicates))) if c.user_is_admin: buttons += [info_button('details')] - if c.user_is_sponsor: - if self.link.promoted is not None: - buttons += [info_button('traffic')] - if len(self.link.title) < 200 and g.spreadshirt_url: - buttons += [info_button('shirt')] - + + # should we show a traffic tab (promoted and author or sponsor) + if (self.link.promoted is not None and + (c.user_is_sponsor or + (c.user_is_loggedin and c.user._id == self.link.author_id))): + buttons += [info_button('traffic')] toolbar = [NavMenu(buttons, base_path = "", type="tabmenu")] @@ -703,13 +769,12 @@ class SubredditsPage(Reddit): if c.user_is_admin: buttons.append(NamedButton("banned")) - #removing the 'my reddits' listing for now - #if c.user_is_loggedin: - # #add the aliases to "my reddits" stays highlighted - # buttons.append(NamedButton("mine", aliases=['/reddits/mine/subscriber', - # '/reddits/mine/contributor', - # '/reddits/mine/moderator'])) - + if c.user_is_loggedin: + #add the aliases to "my reddits" stays highlighted + buttons.append(NamedButton("mine", + aliases=['/reddits/mine/subscriber', + '/reddits/mine/contributor', + '/reddits/mine/moderator'])) return [PageNameNav('reddits'), NavMenu(buttons, base_path = '/reddits', type="tabmenu")] @@ -720,7 +785,7 @@ class SubredditsPage(Reddit): def rightbox(self): ps = Reddit.rightbox(self) - ps.append(SubscriptionBox()) + ps.append(SideContentBox(_("your front page reddits"), [SubscriptionBox()])) return ps class MySubredditsPage(SubredditsPage): @@ -757,7 +822,7 @@ class ProfilePage(Reddit): def build_toolbars(self): path = "/user/%s/" % self.user.name main_buttons = [NavButton(menu.overview, '/', aliases = ['/overview']), - NavButton(plurals.comments, 'comments'), + NamedButton('comments'), NamedButton('submitted')] if votes_visible(self.user): @@ -781,15 +846,30 @@ class ProfilePage(Reddit): if c.user_is_admin: from admin_pages import AdminSidebar rb.append(AdminSidebar(self.user)) + if g.show_awards: + tc = TrophyCase(self.user) + helplink = ( "/help/awards", _("what's this?") ) + scb = SideContentBox(title=_("trophy case"), + helplink=helplink, content=[tc], + extra_class="trophy-area") + rb.append(scb) return rb +class TrophyCase(Templated): + def __init__(self, user): + self.user = user + self.trophies = Trophy.by_account(user) + self.cup_date = user.should_show_cup() + Templated.__init__(self) + class ProfileBar(Templated): """Draws a right box for info about the user (karma, etc)""" def __init__(self, user): Templated.__init__(self, user = user) - self.isFriend = self.user._id in c.user.friends \ - if c.user_is_loggedin else False - self.isMe = (self.user == c.user) + self.is_friend = None + self.my_fullname = c.user_is_loggedin and c.user._fullname + if c.user_is_loggedin: + self.is_friend = self.user._id in c.user.friends class MenuArea(Templated): """Draws the gray box at the top of a page for sort menus""" @@ -848,8 +928,10 @@ class SubredditTopBar(Templated): self.my_reddits = Subreddit.user_subreddits(c.user, ids = False) - self.pop_reddits = Subreddit.default_subreddits(ids = False, - limit = Subreddit.sr_limit) + p_srs = Subreddit.default_subreddits(ids = False, + limit = Subreddit.sr_limit) + self.pop_reddits = [ sr for sr in p_srs if sr.name not in g.automatic_reddits ] + # This doesn't actually work. # self.reddits = c.recent_reddits @@ -869,11 +951,13 @@ class SubredditTopBar(Templated): type = 'srdrop') def subscribed_reddits(self): - return NavMenu([SubredditButton(sr) for sr in + srs = [SubredditButton(sr) for sr in sorted(self.my_reddits, key = lambda sr: sr._downs, reverse=True) - ], + if sr.name not in g.automatic_reddits + ] + return NavMenu(srs, type='flatlist', separator = '-', _id = 'sr-bar') @@ -933,9 +1017,10 @@ class CssError(Templated): class UploadedImage(Templated): "The page rendered in the iframe during an upload of a header image" - def __init__(self,status,img_src, name="", errors = {}): + def __init__(self,status,img_src, name="", errors = {}, form_id = ""): self.errors = list(errors.iteritems()) - Templated.__init__(self, status=status, img_src=img_src, name = name) + Templated.__init__(self, status=status, img_src=img_src, name = name, + form_id = form_id) class Password(Templated): """Form encountered when 'recover password' is clicked in the LoginFormWide.""" @@ -948,6 +1033,12 @@ class PasswordReset(Templated): entered their user name in Password.)""" pass +class VerifyEmail(Templated): + pass + +class Promo_Email(Templated): + pass + class ResetPassword(Templated): """Form for actually resetting a lost password, after the user has clicked on the link provided to them in the Password_Reset email @@ -1153,28 +1244,6 @@ class OptIn(Templated): pass -class UserStats(Templated): - """For drawing the stats page, which is fetched from the cache.""" - def __init__(self): - Templated.__init__(self) - cache_stats = cache.get('stats') - if cache_stats: - top_users, top_day, top_week = cache_stats - - #lookup user objs - uids = [] - uids.extend(u for u in top_users) - uids.extend(u[0] for u in top_day) - uids.extend(u[0] for u in top_week) - users = Account._byID(uids, data = True) - - self.top_users = (users[u] for u in top_users) - self.top_day = ((users[u[0]], u[1]) for u in top_day) - self.top_week = ((users[u[0]], u[1]) for u in top_week) - else: - self.top_users = self.top_day = self.top_week = () - - class ButtonEmbed(Templated): """Generates the JS wrapper around the buttons for embedding.""" def __init__(self, button = None, width = 100, @@ -1182,7 +1251,7 @@ class ButtonEmbed(Templated): Templated.__init__(self, button = button, width = width, height = height, referer=referer, url = url, **kw) - + class Button(Wrapped): cachable = True extension_handling = False @@ -1190,14 +1259,16 @@ class Button(Wrapped): Wrapped.__init__(self, link, **kw) if link is None: self.add_props(c.user, [self]) - - + + @classmethod def add_props(cls, user, wrapped): # unlike most wrappers we can guarantee that there is a link # that this wrapper is wrapping. Link.add_props(user, [w for w in wrapped if hasattr(w, "_fullname")]) for w in wrapped: + # caching: store the user name since each button has a modhash + w.user_name = c.user.name if c.user_is_loggedin else "" if not hasattr(w, '_fullname'): w._fullname = None @@ -1214,6 +1285,9 @@ class ButtonDemoPanel(Templated): pass +class SelfServeBlurb(Templated): + pass + class Feedback(Templated): """The feedback and ad inquery form(s)""" def __init__(self, title, action): @@ -1259,7 +1333,48 @@ class AdminTranslations(Templated): from r2.lib.translation import list_translations Templated.__init__(self) self.translations = list_translations() - + +class UserAwards(Templated): + """For drawing the regular-user awards page.""" + def __init__(self): + from r2.models import Award, Trophy + Templated.__init__(self) + + if not g.show_awards: + abort(404, "not found"); + + self.winners = [] + for award in Award._all_awards(): + trophies = Trophy.by_award(award) + # Don't show awards that nobody's ever won + # (e.g., "9-Year Club") + if trophies: + winner = trophies[0]._thing1.name + self.winners.append( (award, winner, trophies[0]) ) + + +class AdminAwards(Templated): + """The admin page for editing awards""" + def __init__(self): + from r2.models import Award + Templated.__init__(self) + self.awards = Award._all_awards() + +class AdminAwardGive(Templated): + """The interface for giving an award""" + def __init__(self, award): + now = datetime.datetime.now(g.display_tz) + self.description = "??? -- " + now.strftime("%Y-%m-%d") + self.url = "" + + Templated.__init__(self, award = award) + +class AdminAwardWinners(Templated): + """The list of winners of an award""" + def __init__(self, award): + trophies = Trophy.by_award(award) + Templated.__init__(self, award = award, trophies = trophies) + class Embed(Templated): """wrapper for embedding /help into reddit as if it were not on a separate wiki.""" @@ -1272,6 +1387,38 @@ class Page_down(Templated): message = kw.get('message', _("This feature is currently unavailable. Sorry")) Templated.__init__(self, message = message) +class WrappedUser(CachedTemplate): + def __init__(self, user, attribs = [], context_thing = None, gray = False): + attribs.sort() + author_cls = 'author' + + if gray: + author_cls += ' gray' + for tup in attribs: + author_cls += " " + tup[2] + + target = None + ip_span = None + context_deleted = None + if context_thing: + target = getattr(context_thing, 'target', None) + ip_span = getattr(context_thing, 'ip_span', None) + context_deleted = context_thing.deleted + + karma = '' + if c.user_is_admin: + karma = ' (%d)' % user.link_karma + + CachedTemplate.__init__(self, + name = user.name, + author_cls = author_cls, + attribs = attribs, + context_thing = context_thing, + karma = karma, + ip_span = ip_span, + context_deleted = context_deleted, + user_deleted = user._deleted) + # Classes for dealing with friend/moderator/contributor/banned lists @@ -1281,10 +1428,12 @@ class UserTableItem(Templated): will determine what order the different columns are rendered in. Currently, this list can consist of 'user', 'sendmessage' and 'remove'.""" - def __init__(self, user, type, cellnames, container_name, editable): + def __init__(self, user, type, cellnames, container_name, editable, + remove_action): self.user, self.type, self.cells = user, type, cellnames self.container_name = container_name self.editable = editable + self.remove_action = remove_action Templated.__init__(self) def __repr__(self): @@ -1298,6 +1447,8 @@ class UserList(Templated): container_name = '' cells = ('user', 'sendmessage', 'remove') _class = "" + destination = "friend" + remove_action = "unfriend" def __init__(self, editable = True): self.editable = editable @@ -1308,7 +1459,7 @@ class UserList(Templated): instance of the user with type, container_name, etc. of this UserList instance""" return UserTableItem(user, self.type, self.cells, self.container_name, - self.editable) + self.editable, self.remove_action) @property def users(self, site = None): @@ -1394,6 +1545,32 @@ class BannedList(UserList): def user_ids(self): return c.site.banned +class TrafficViewerList(UserList): + """Traffic share list on /traffic/*""" + destination = "traffic_viewer" + remove_action = "rm_traffic_viewer" + type = 'traffic' + + def __init__(self, link, editable = True): + self.link = link + UserList.__init__(self, editable = editable) + + @property + def form_title(self): + return _('share traffic') + + @property + def table_title(self): + return _('current viewers') + + def user_ids(self): + return promote.traffic_viewers(self.link) + + @property + def container_name(self): + return self.link._fullname + + class DetailsPage(LinkInfoPage): extension_handling= False @@ -1425,41 +1602,66 @@ class PromotePage(Reddit): create_reddit_box = False submit_box = False extension_handling = False + searchbox = False def __init__(self, title, nav_menus = None, *a, **kw): - buttons = [NamedButton('current_promos', dest = ''), - NamedButton('new_promo')] + buttons = [NamedButton('new_promo')] + if c.user_is_admin: + buttons.append(NamedButton('current_promos', dest = '')) + else: + buttons.append(NamedButton('my_current_promos', dest = '')) - menu = NavMenu(buttons, base_path = '/promote', type='flatlist') + buttons += [NamedButton('future_promos'), + NamedButton('unpaid_promos'), + NamedButton('pending_promos'), + NamedButton('live_promos')] + + if c.user_is_sponsor or c.user_is_paid_sponsor: + buttons.append(NamedButton('graph')) + + menu = NavMenu(buttons, base_path = '/promoted', + type='flatlist') if nav_menus: nav_menus.insert(0, menu) else: nav_menus = [menu] + kw['show_sidebar'] = False Reddit.__init__(self, title, nav_menus = nav_menus, *a, **kw) - -class PromotedLinks(Templated): - def __init__(self, current_list, *a, **kw): - self.things = current_list - - self.recent = dict(load_summary("thing")) - - if self.recent: - link_listing = wrap_links(self.recent.keys()) - for t in link_listing: - self.recent[t._fullname].insert(0, t) - - self.recent = self.recent.values() - self.recent.sort(key = lambda x: x[0]._date) - Templated.__init__(self, datefmt = datefmt, *a, **kw) - class PromoteLinkForm(Templated): def __init__(self, sr = None, link = None, listing = '', timedeltatext = '', *a, **kw): + bids = [] + if c.user_is_admin and link: + try: + bids = bidding.Bid.lookup(thing_id = link._id) + bids.sort(key = lambda x: x.date, reverse = True) + except NotFound: + bids = [] + + # reference "now" to what we use for promtions + now = promote.promo_datetime_now() + + # min date is the day before the first possible start date. + mindate = (make_offset_date(now, g.min_promote_future, + business_days = True) - + datetime.timedelta(1)) + + if link: + startdate = link._date + enddate = link.promote_until + else: + startdate = mindate + datetime.timedelta(1) + enddate = startdate + datetime.timedelta(1) + + self.startdate = startdate.strftime("%m/%d/%Y") + self.enddate = enddate .strftime("%m/%d/%Y") + self.mindate = mindate .strftime("%m/%d/%Y") + Templated.__init__(self, sr = sr, link = link, - datefmt = datefmt, + datefmt = datefmt, bids = bids, timedeltatext = timedeltatext, listing = listing, *a, **kw) @@ -1528,7 +1730,8 @@ class UserText(CachedTemplate): display = True, post_form = 'editusertext', cloneable = False, - extra_css = ''): + extra_css = '', + name = "text"): css_class = "usertext" if cloneable: @@ -1547,7 +1750,8 @@ class UserText(CachedTemplate): display = display, post_form = post_form, cloneable = cloneable, - css_class = css_class) + css_class = css_class, + name = name) class MediaEmbedBody(CachedTemplate): """What's rendered inside the iframe that contains media objects""" @@ -1567,44 +1771,78 @@ class PromotedTraffic(Traffic): """ def __init__(self, thing): self.thing = thing - d = thing._date.astimezone(g.tz) + d = thing._date.astimezone(g.tz) - promote.timezone_offset d = d.replace(minute = 0, second = 0, microsecond = 0) - - until = thing.promote_until + until = thing.promote_until - promote.timezone_offset now = datetime.datetime.now(g.tz) - if not until: - until = d + datetime.timedelta(1) - if until > now: - until - now - + + # the results are preliminary until 1 day after the promotion ends + self.preliminary = (until + datetime.timedelta(1) > now) + self.traffic = load_traffic('hour', "thing", thing._fullname, start_time = d, stop_time = until) + # load monthly totals if we have them, otherwise use the daily totals self.totals = load_traffic('month', "thing", thing._fullname) if not self.totals: self.totals = load_traffic('day', "thing", thing._fullname) + # generate a list of + # (uniq impressions, # impressions, uniq clicks, # clicks) if self.totals: self.totals = map(sum, zip(*zip(*self.totals)[1])) - + imp = self.slice_traffic(self.traffic, 0, 1) if len(imp) > 2: - imp_total = locale.format('%d', sum(x[2] for x in imp), True) + imp_total = sum(x[2] for x in imp) + # ensure total consistency: + if self.totals: + self.totals[1] = imp_total + + imp_total = locale.format('%d', imp_total, True) chart = graph.LineGraph(imp) self.imp_graph = chart.google_chart(ylabels = ['uniques', 'total'], title = ("impressions (%s)" % imp_total)) - + cli = self.slice_traffic(self.traffic, 2, 3) - cli_total = locale.format('%d', sum(x[2] for x in cli), True) + cli_total = sum(x[2] for x in cli) + # ensure total consistency + if self.totals: + self.totals[3] = cli_total + cli_total = locale.format('%d', cli_total, True) chart = graph.LineGraph(cli) self.cli_graph = chart.google_chart(ylabels = ['uniques', 'total'], title = ("clicks (%s)" % cli_total)) else: self.imp_graph = self.cli_graph = None + + editable = c.user_is_sponsor or c.user._id == thing.author_id + self.viewers = TrafficViewerList(thing, editable = editable) Templated.__init__(self) + def to_iter(self, localize = True, total = False): + def num(x): + if localize: + return locale.format('%d', x, True) + return str(x) + def row(label, data): + uimp, nimp, ucli, ncli = data + return (label, + num(uimp), num(nimp), num(ucli), num(ncli), + ("%.2f%%" % (float(100*ucli) / uimp)) if nimp else "--.--%", + ("%.2f%%" % (float(100*ncli) / nimp)) if nimp else "--.--%") + + for date, data in self.traffic: + yield row(date.strftime("%Y-%m-%d %H:%M"), data) + if total: + yield row("total", self.totals) + + + def as_csv(self): + return to_csv(self.to_iter(localize = False, total = True)) + class RedditTraffic(Traffic): """ fetches hourly and daily traffic for the current reddit. If the @@ -1679,7 +1917,7 @@ class RedditTraffic(Traffic): d[0] = name return data return res - + def monthly_summary(self): """ Convenience method b/c it is bad form to do this much math @@ -1688,31 +1926,235 @@ class RedditTraffic(Traffic): res = [] if c.default_sr: data = self.month_data + + # figure out the mean number of users last month, + # unless today is the first and there is no data + days = self.day_data + now = datetime.datetime.utcnow() + if now.day != 1: + # project based on traffic so far + # totals are going to be up to yesterday + month_len = calendar.monthrange(now.year, now.month)[1] + + lastmonth = datetime.datetime.utcnow().month + lastmonthyear = datetime.datetime.utcnow().year + if lastmonth == 1: + lastmonthyear -= 1 + lastmonth = 1 + else: + lastmonth = (lastmonth - 1) if lastmonth != 1 else 12 + # length of last month + lastmonthlen = calendar.monthrange(lastmonthyear, lastmonth)[1] + + lastdays = filter(lambda x: x[0].month == lastmonth, days) + thisdays = filter(lambda x: x[0].month == now.month, days) + user_scale = 0 + if lastdays: + last_mean = (sum(u for (d, (u, v)) in lastdays) / + float(len(lastdays))) + day_mean = (sum(u for (d, (u, v)) in thisdays) / + float(len(thisdays))) + if last_mean and day_mean: + user_scale = ( (day_mean * month_len) / + (last_mean * lastmonthlen) ) + last_month_users = 0 for x, (date, d) in enumerate(data): res.append([("date", date.strftime("%Y-%m")), ("", locale.format("%d", d[0], True)), ("", locale.format("%d", d[1], True))]) last_d = data[x-1][1] if x else None for i in range(2): + # store last month's users for this month's projection + if x == len(data) - 2 and i == 0: + last_month_users = d[i] if x == 0: res[-1].append(("","")) - elif x == len(data) - 1: - # project based on traffic so far - # totals are going to be up to yesterday - month_len = calendar.monthrange(date.year, - date.month)[1] + # project, unless today is the first of the month + elif x == len(data) - 1 and now.day != 1: + # yesterday yday = (datetime.datetime.utcnow() -datetime.timedelta(1)).day - scaled = float(d[i] * month_len) / yday + if i == 0: + scaled = int(last_month_users * user_scale) + else: + scaled = float(d[i] * month_len) / yday res[-1].append(("gray", locale.format("%d", scaled, True))) elif last_d and d[i] and last_d[i]: f = 100 * (float(d[i])/last_d[i] - 1) - + res[-1].append(("up" if f > 0 else "down", "%5.2f%%" % f)) return res +class PaymentForm(Templated): + def __init__(self, **kw): + self.countries = pycountry.countries + Templated.__init__(self, **kw) + +class Promote_Graph(Templated): + def __init__(self): + self.now = promote.promo_datetime_now() + start_date = (self.now - datetime.timedelta(7)).date() + end_date = (self.now + datetime.timedelta(7)).date() + + size = (end_date - start_date).days + + # grab promoted links + promos = PromoteDates.for_date_range(start_date, end_date) + promos.sort(key = lambda x: x.start_date) + + # wrap the links + links = wrap_links([p.thing_name for p in promos]) + # remove rejected/unpaid promos + links = dict((l._fullname, l) for l in links.things + if (l.promoted is not None and + l.promote_status not in ( promote.STATUS.rejected, + promote.STATUS.unpaid)) ) + # filter promos accordingly + promos = filter(lambda p: links.has_key(p.thing_name), promos) + + promote_blocks = [] + market = {} + my_market = {} + promo_counter = {} + for p in promos: + starti = max((p.start_date - start_date).days, 0) + endi = min((p.end_date - start_date).days, size) + link = links[p.thing_name] + bid_day = link.promote_bid/max((p.end_date - p.start_date).days, 1) + for i in xrange(starti, endi): + market[i] = market.get(i, 0) + bid_day + if c.user_is_sponsor or link.author_id == c.user._id: + my_market[i] = my_market.get(i, 0) + bid_day + promo_counter[i] = promo_counter.get(i, 0) + 1 + if c.user_is_sponsor or link.author_id == c.user._id: + promote_blocks.append( (link, starti, endi) ) + + # now sort the promoted_blocks into the most contiguous chuncks we can + sorted_blocks = [] + while promote_blocks: + cur = promote_blocks.pop(0) + while True: + sorted_blocks.append(cur) + # get the future items (sort will be preserved) + future = filter(lambda x: x[1] >= cur[2], promote_blocks) + if future: + # resort by date and give precidence to longest promo: + cur = min(future, key = lambda x: (x[1], x[1]-x[2])) + promote_blocks.remove(cur) + else: + break + + # load recent traffic as well: + self.recent = {} + for k, v in load_summary("thing"): + if k.startswith('t%d_' % Link._type_id): + self.recent[k] = v + + if self.recent: + link_listing = wrap_links(self.recent.keys()) + for t in link_listing: + self.recent[t._fullname].insert(0, t) + + self.recent = self.recent.values() + self.recent.sort(key = lambda x: x[0]._date) + + # graphs of money + history = self.now - datetime.timedelta(60) + pool = bidding.PromoteDates.bid_history(history) + if pool: + # we want to generate a stacked line graph, so store the + # bids and the total including refunded amounts + chart = graph.LineGraph([(d, b, r) for (d, b, r) in pool], + colors = ("008800", "FF0000")) + total_sale = sum(b for (d, b, r) in pool) + total_refund = sum(r for (d, b, r) in pool) + self.money_graph = chart.google_chart( + ylabels = ['total ($)'], + title = ("monthly sales ($%.2f total, $%.2f credits)" % + (total_sale, total_refund)), + multiy = False) + + self.top_promoters = bidding.PromoteDates.top_promoters(history) + else: + self.money_graph = None + self.top_promoters = [] + + # graphs of impressions and clicks + self.promo_traffic = load_traffic('day', 'promos') + impressions = [(d, i) for (d, (i, k)) in self.promo_traffic] + + pool = dict((d, b+r) for (d, b, r) in pool) + + if impressions: + chart = graph.LineGraph(impressions) + self.imp_graph = chart.google_chart(ylabels = ['total'], + title = "impressions") + + clicks = [(d, k) for (d, (i, k)) in self.promo_traffic] + + CPM = [(d, (pool.get(d, 0) * 1000. / i) if i else 0) + for (d, (i, k)) in self.promo_traffic] + + CPC = [(d, (100 * pool.get(d, 0) / k) if k else 0) + for (d, (i, k)) in self.promo_traffic] + + CTR = [(d, (100 * float(k) / i if i else 0)) + for (d, (i, k)) in self.promo_traffic] + + chart = graph.LineGraph(clicks) + self.cli_graph = chart.google_chart(ylabels = ['total'], + title = "clicks") + + mean_CPM = sum(x[1] for x in CPM) * 1. / max(len(CPM), 1) + chart = graph.LineGraph([(d, min(x, mean_CPM*2)) for d, x in CPM], + colors = ["336699"]) + self.cpm_graph = chart.google_chart(ylabels = ['CPM ($)'], + title = "cost per 1k impressions " + + "($%.2f average)" % mean_CPM) + + mean_CPC = sum(x[1] for x in CPC) * 1. / max(len(CPC), 1) + chart = graph.LineGraph([(d, min(x, mean_CPC*2)) for d, x in CPC], + colors = ["336699"]) + self.cpc_graph = chart.google_chart(ylabels = ['CPC ($0.01)'], + title = "cost per click " + + "($%.2f average)" % (mean_CPC/100.)) + + chart = graph.LineGraph(CTR, colors = ["336699"]) + self.ctr_graph = chart.google_chart(ylabels = ['CTR (%)'], + title = "click through rate") + + else: + self.imp_graph = self.cli_graph = None + self.cpc_graph = self.cpm_graph = None + self.ctr_graph = None + + self.promo_traffic = dict(self.promo_traffic) + + Templated.__init__(self, + total_size = size, + market = market, + my_market = my_market, + promo_counter = promo_counter, + start_date = start_date, + promote_blocks = sorted_blocks) + + def to_iter(self, localize = True): + def num(x): + if localize: + return locale.format('%d', x, True) + return str(x) + for link, uimp, nimp, ucli, ncli in self.recent: + yield (link._date.strftime("%Y-%m-%d"), + num(uimp), num(nimp), num(ucli), num(ncli), + num(link._ups - link._downs), + "$%.2f" % link.promote_bid, + _force_unicode(link.title)) + + def as_csv(self): + return to_csv(self.to_iter(localize = False)) + class InnerToolbarFrame(Templated): def __init__(self, link, expanded = False): Templated.__init__(self, link = link, expanded = expanded) @@ -1723,3 +2165,10 @@ class RawString(Templated): def render(self, *a, **kw): return unsafe(self.s) + +class Dart_Ad(Templated): + def __init__(self, tag = None): + tag = tag or "homepage" + tracker_url = AdframeInfo.gen_url(fullname = "dart_" + tag, + ip = request.ip) + Templated.__init__(self, tag = tag, tracker_url = tracker_url) diff --git a/r2/r2/lib/pages/things.py b/r2/r2/lib/pages/things.py index 5784ebd45..42013bdfe 100644 --- a/r2/r2/lib/pages/things.py +++ b/r2/r2/lib/pages/things.py @@ -24,7 +24,9 @@ from r2.lib.wrapped import Wrapped from r2.models import LinkListing, make_wrapper, Link, IDBuilder, PromotedLink, Thing from r2.lib.utils import tup from r2.lib.strings import Score -from pylons import c +from r2.lib.promote import promo_edit_url, promo_traffic_url +from datetime import datetime +from pylons import c, g class PrintableButtons(Styled): def __init__(self, style, thing, @@ -61,6 +63,18 @@ class LinkButtons(PrintableButtons): show_distinguish = (is_author and thing.can_ban and getattr(thing, "expand_children", False)) + kw = {} + if thing.promoted is not None: + now = datetime.now(g.tz) + promotable = (thing._date <= now and thing.promote_until > now) + kw = dict(promo_url = promo_edit_url(thing), + promote_bid = thing.promote_bid, + promote_status = getattr(thing, "promote_status", 0), + user_is_sponsor = c.user_is_sponsor, + promotable = promotable, + traffic_url = promo_traffic_url(thing), + is_author = thing.is_author) + PrintableButtons.__init__(self, 'linkbuttons', thing, # user existence and preferences is_loggedin = c.user_is_loggedin, @@ -76,7 +90,10 @@ class LinkButtons(PrintableButtons): show_delete = show_delete, show_report = show_report, show_distinguish = show_distinguish, - show_comments = comments) + show_comments = comments, + # promotion + promoted = thing.promoted, + **kw) class CommentButtons(PrintableButtons): def __init__(self, thing, delete = True, report = True): @@ -105,12 +122,13 @@ class MessageButtons(PrintableButtons): def __init__(self, thing, delete = False, report = True): was_comment = getattr(thing, 'was_comment', False) permalink = thing.permalink if was_comment else "" - + PrintableButtons.__init__(self, "messagebuttons", thing, profilepage = c.profilepage, permalink = permalink, - was_comment = was_comment, + was_comment = was_comment, can_reply = c.user_is_loggedin, + parent_id = getattr(thing, "parent_id", None), show_report = True, show_delete = False) @@ -120,7 +138,7 @@ def default_thing_wrapper(**params): w = Wrapped(thing) style = params.get('style', c.render_style) if isinstance(thing, Link): - if thing.promoted: + if thing.promoted is not None: w.render_class = PromotedLink w.rowstyle = 'promoted link' elif style == 'htmllite': diff --git a/r2/r2/lib/promote.py b/r2/r2/lib/promote.py index 90738ea2a..aea5a463b 100644 --- a/r2/r2/lib/promote.py +++ b/r2/r2/lib/promote.py @@ -22,50 +22,426 @@ from __future__ import with_statement from r2.models import * +from r2.lib import authorize +from r2.lib import emailer, filters from r2.lib.memoize import memoize - -from datetime import datetime +from r2.lib.template_helpers import get_domain +from r2.lib.utils import Enum +from pylons import g, c +from datetime import datetime, timedelta +import random promoted_memo_lifetime = 30 -promoted_memo_key = 'cached_promoted_links' -promoted_lock_key = 'cached_promoted_links_lock' +promoted_memo_key = 'cached_promoted_links2' +promoted_lock_key = 'cached_promoted_links_lock2' -def promote(thing, subscribers_only = False, promote_until = None, - disable_comments = False): +STATUS = Enum("unpaid", "unseen", "accepted", "rejected", + "pending", "promoted", "finished") - thing.promoted = True - thing.promoted_on = datetime.now(g.tz) +PromoteSR = 'promos' +try: + PromoteSR = Subreddit._new(name = PromoteSR, + title = "promoted links", + author_id = -1, + type = "public", + ip = '0.0.0.0') +except SubredditExists: + PromoteSR = Subreddit._by_name(PromoteSR) - if c.user: - thing.promoted_by = c.user._id +def promo_traffic_url(l): + domain = get_domain(cname = False, subreddit = False) + return "http://%s/traffic/%s/" % (domain, l._id36) - if promote_until: - thing.promote_until = promote_until +def promo_edit_url(l): + domain = get_domain(cname = False, subreddit = False) + return "http://%s/promoted/edit_promo/%s" % (domain, l._id36) - if disable_comments: - thing.disable_comments = True +# These could be done with relationships, but that seeks overkill as +# we never query based on user and only check per-thing +def is_traffic_viewer(thing, user): + return (c.user_is_sponsor or user._id == thing.author_id or + user._id in getattr(thing, "promo_traffic_viewers", set())) - if subscribers_only: - thing.promoted_subscribersonly = True +def add_traffic_viewer(thing, user): + viewers = getattr(thing, "promo_traffic_viewers", set()).copy() + if user._id not in viewers: + viewers.add(user._id) + thing.promo_traffic_viewers = viewers + thing._commit() + return True + return False +def rm_traffic_viewer(thing, user): + viewers = getattr(thing, "promo_traffic_viewers", set()).copy() + if user._id in viewers: + viewers.remove(user._id) + thing.promo_traffic_viewers = viewers + thing._commit() + return True + return False + +def traffic_viewers(thing): + return sorted(getattr(thing, "promo_traffic_viewers", set())) + +# logging routine for keeping track of diffs +def promotion_log(thing, text, commit = False): + """ + For logging all sorts of things + """ + name = c.user.name if c.user_is_loggedin else "" + log = list(getattr(thing, "promotion_log", [])) + now = datetime.now(g.tz).strftime("%Y-%m-%d %H:%M:%S") + text = "[%s: %s] %s" % (name, now, text) + log.append(text) + # copy (and fix encoding) to make _dirty + thing.promotion_log = map(filters._force_utf8, log) + if commit: + thing._commit() + return text + +def new_promotion(title, url, user, ip, promote_start, promote_until, bid, + disable_comments = False, + max_clicks = None, max_views = None): + """ + Creates a new promotion with the provided title, etc, and sets it + status to be 'unpaid'. + """ + l = Link._submit(title, url, user, PromoteSR, ip) + l.promoted = True + l.promote_until = None + l.promote_status = STATUS.unpaid + l.promote_trans_id = 0 + l.promote_bid = bid + l.maximum_clicks = max_clicks + l.maximum_views = max_views + l.disable_comments = disable_comments + update_promo_dates(l, promote_start, promote_until) + promotion_log(l, "promotion created") + l._commit() + # the user has posted a promotion, so enable the promote menu unless + # they have already opted out + if user.pref_show_promote is not False: + user.pref_show_promote = True + user._commit() + emailer.new_promo(l) + return l + +def update_promo_dates(thing, start_date, end_date, commit = True): + if thing and thing.promote_status < STATUS.pending or c.user_is_admin: + if (thing._date != start_date or + thing.promote_until != end_date): + promotion_log(thing, "duration updated (was %s -> %s)" % + (thing._date, thing.promote_until)) + thing._date = start_date + thing.promote_until = end_date + PromoteDates.update(thing, start_date, end_date) + if commit: + thing._commit() + return True + return False + +def update_promo_data(thing, title, url, commit = True): + if thing and (thing.url != url or thing.title != title): + if thing.title != title: + promotion_log(thing, "title updated (was '%s')" % + thing.title) + if thing.url != url: + promotion_log(thing, "url updated (was '%s')" % + thing.url) + old_url = thing.url + thing.url = url + thing.title = title + if not c.user_is_sponsor: + unapproved_promo(thing) + thing.update_url_cache(old_url) + if commit: + thing._commit() + return True + return False + +def refund_promo(thing, user, refund): + cur_refund = getattr(thing, "promo_refund", 0) + refund = min(refund, thing.promote_bid - cur_refund) + if refund > 0: + thing.promo_refund = cur_refund + refund + if authorize.refund_transaction(refund, user, thing.promote_trans_id): + promotion_log(thing, "payment update: refunded '%.2f'" % refund) + else: + promotion_log(thing, "payment update: refund failed") + if thing.promote_status in (STATUS.promoted, STATUS.finished): + PromoteDates.update_bid(thing) + thing._commit() + +def auth_paid_promo(thing, user, pay_id, bid): + """ + promotes a promotion from 'unpaid' to 'unseen'. + + In the case that bid already exists on the current promotion, the + previous transaction is voided and repalced with the new bid. + """ + if thing.promote_status == STATUS.finished: + return + elif (thing.promote_status > STATUS.unpaid and + thing.promote_trans_id): + # void the existing transaction + authorize.void_transaction(user, thing.promote_trans_id) + + # create a new transaction and update the bid + trans_id = authorize.auth_transaction(bid, user, pay_id, thing) + thing.promote_bid = bid + + if trans_id is not None: + # we won't reset to unseen if already approved and the payment went ok + promotion_log(thing, "updated payment and/or bid: SUCCESS") + if trans_id < 0: + promotion_log(thing, "FREEBIE") + thing.promote_status = max(thing.promote_status, STATUS.unseen) + thing.promote_trans_id = trans_id + else: + # something bad happend. + promotion_log(thing, "updated payment and/or bid: FAILED") + thing.promore_status = STATUS.unpaid + thing.promote_trans_id = 0 + PromoteDates.update_bid(thing) + # commit last to guarantee consistency thing._commit() + emailer.promo_bid(thing) + return bool(trans_id) - with g.make_lock(promoted_lock_key): - promoted = get_promoted_direct() - promoted.append(thing._fullname) - set_promoted(promoted) + +def unapproved_promo(thing): + """ + revert status of a promoted link to unseen. -def unpromote(thing): + NOTE: if the promotion is live, this has the side effect of + bumping it from the live queue pending an admin's intervention to + put it back in place. + """ + # only reinforce pending if it hasn't been seen yet. + if STATUS.unseen < thing.promote_status < STATUS.finished: + promotion_log(thing, "status update: unapproved") + unpromote(thing, status = STATUS.unseen) + +def accept_promo(thing): + """ + Accept promotion and set its status as accepted if not already + charged, else pending. + """ + if thing.promote_status < STATUS.pending: + bid = Bid.one(thing.promote_trans_id) + if bid.status == Bid.STATUS.CHARGE: + thing.promote_status = STATUS.pending + # repromote if already promoted before + if hasattr(thing, "promoted_on"): + promote(thing) + else: + emailer.queue_promo(thing) + else: + thing.promote_status = STATUS.accepted + promotion_log(thing, "status update: accepted") + emailer.accept_promo(thing) + thing._commit() + +def reject_promo(thing, reason = ""): + """ + Reject promotion and set its status as rejected + + Here, we use unpromote so that we can also remove a promotion from + the queue if it has become promoted. + """ + unpromote(thing, status = STATUS.rejected) + promotion_log(thing, "status update: rejected. Reason: '%s'" % reason) + emailer.reject_promo(thing, reason) + +def delete_promo(thing): + """ + deleted promotions have to be specially dealt with. Reject the + promo and void any associated transactions. + """ thing.promoted = False - thing.unpromoted_on = datetime.now(g.tz) - thing.promote_until = None + thing._deleted = True + reject_promo(thing, reason = "The promotion was deleted by the user") + if thing.promote_trans_id > 0: + user = Account._byID(thing.author_id) + authorize.void_transaction(user, thing.promote_trans_id) + + + +def pending_promo(thing): + """ + For an accepted promotion within the proper time interval, charge + the account of the user and set the new status as pending. + """ + if thing.promote_status == STATUS.accepted and thing.promote_trans_id: + user = Account._byID(thing.author_id) + # TODO: check for charge failures/recharges, etc + if authorize.charge_transaction(user, thing.promote_trans_id): + promotion_log(thing, "status update: pending") + thing.promote_status = STATUS.pending + thing.promote_paid = thing.promote_bid + thing._commit() + emailer.queue_promo(thing) + else: + promotion_log(thing, "status update: charge failure") + thing._commit() + #TODO: email rejection? + + + +def promote(thing, batch = False): + """ + Given a promotion with pending status, set the status to promoted + and move it into the promoted queue. + """ + if thing.promote_status == STATUS.pending: + promotion_log(thing, "status update: live") + PromoteDates.log_start(thing) + thing.promoted_on = datetime.now(g.tz) + thing.promote_status = STATUS.promoted + thing._commit() + emailer.live_promo(thing) + if not batch: + with g.make_lock(promoted_lock_key): + promoted = get_promoted_direct() + if thing._fullname not in promoted: + promoted[thing._fullname] = auction_weight(thing) + set_promoted(promoted) + +def unpromote(thing, batch = False, status = STATUS.finished): + """ + unpromote a link with provided status, removing it from the + current promotional queue. + """ + if status == STATUS.finished: + PromoteDates.log_end(thing) + emailer.finished_promo(thing) + thing.unpromoted_on = datetime.now(g.tz) + promotion_log(thing, "status update: finished") + thing.promote_status = status thing._commit() + if not batch: + with g.make_lock(promoted_lock_key): + promoted = get_promoted_direct() + if thing._fullname in promoted: + del promoted[thing._fullname] + set_promoted(promoted) +# batch methods for moving promotions into the pending queue, and +# setting status as pending. + +# dates are referenced to UTC, while we want promos to change at (roughly) +# midnight eastern-US. +# TODO: make this a config parameter +timezone_offset = -5 # hours +timezone_offset = timedelta(0, timezone_offset * 3600) + +def promo_datetime_now(): + return datetime.now(g.tz) + timezone_offset + +def generate_pending(date = None, test = False): + """ + Look-up links that are to be promoted on the provided date (the + default is now plus one day) and set their status as pending if + they have been accepted. This results in credit cards being charged. + """ + date = date or (promo_datetime_now() + timedelta(1)) + links = Link._by_fullname([p.thing_name for p in + PromoteDates.for_date(date)], + data = True, + return_dict = False) + for l in links: + if l._deleted and l.promote_status != STATUS.rejected: + print "DELETING PROMO", l + # deleted promos should never be made pending + delete_promo(l) + elif l.promote_status == STATUS.accepted: + if test: + print "Would have made pending: (%s, %s)" % \ + (l, l.make_permalink(None)) + else: + pending_promo(l) + + +def promote_promoted(test = False): + """ + make promotions that are no longer supposed to be active + 'finished' and find all pending promotions that are supposed to be + promoted and promote them. + """ + from r2.lib.traffic import load_traffic with g.make_lock(promoted_lock_key): - promoted = [ x for x in get_promoted_direct() - if x != thing._fullname ] + now = promo_datetime_now() - set_promoted(promoted) + promoted = Link._by_fullname(get_promoted_direct().keys(), + data = True, return_dict = False) + promos = {} + for l in promoted: + keep = True + if l.promote_until < now: + keep = False + maximum_clicks = getattr(l, "maximum_clicks", None) + maximum_views = getattr(l, "maximum_views", None) + if maximum_clicks or maximum_views: + # grab the traffic + traffic = load_traffic("day", "thing", l._fullname) + if traffic: + # (unique impressions, number impressions, + # unique clicks, number of clicks) + traffic = [y for x, y in traffic] + traffic = map(sum, zip(*traffic)) + uimp, nimp, ucli, ncli = traffic + if maximum_clicks and maximum_clicks < ncli: + keep = False + if maximum_views and maximum_views < nimp: + keep = False + + if not keep: + if test: + print "Would have unpromoted: (%s, %s)" % \ + (l, l.make_permalink(None)) + else: + unpromote(l, batch = True) + + new_promos = Link._query(Link.c.promote_status == (STATUS.pending, + STATUS.promoted), + Link.c.promoted == True, + data = True) + for l in new_promos: + if l.promote_until > now and l._date <= now: + if test: + print "Would have promoted: %s" % l + else: + promote(l, batch = True) + promos[l._fullname] = auction_weight(l) + elif l.promote_until <= now: + if test: + print "Would have unpromoted: (%s, %s)" % \ + (l, l.make_permalink(None)) + else: + unpromote(l, batch = True) + + # remove unpaid promos that are scheduled to run on today or before + unpaid_promos = Link._query(Link.c.promoted == True, + Link.c.promote_status == STATUS.unpaid, + Link.c._date < now, + Link.c._deleted == False, + data = True) + for l in unpaid_promos: + if test: + print "Would have rejected: %s" % promo_edit_url(l) + else: + reject_promo(l, reason = "We're sorry, but this sponsored link was not set up for payment before the appointed date. Please add payment info and move the date into the future if you would like to resubmit. Also please feel free to email us at selfservicesupport@reddit.com if you believe this email is in error.") + + + if test: + print promos + else: + set_promoted(promos) + return promos + +def auction_weight(link): + duration = (link.promote_until - link._date).days + return duration and link.promote_bid / duration def set_promoted(link_names): # caller is assumed to execute me inside a lock if necessary @@ -81,55 +457,60 @@ def get_promoted(): return get_promoted_direct() def get_promoted_direct(): - return g.permacache.get(promoted_memo_key, []) + return g.permacache.get(promoted_memo_key, {}) -def expire_promoted(): - """ - To be called periodically (e.g. by `cron') to clean up - promoted links past their expiration date - """ - with g.make_lock(promoted_lock_key): - link_names = set(get_promoted_direct()) - links = Link._by_fullname(link_names, data=True, return_dict = False) - - link_names = [] - expired_names = [] - - for x in links: - if (not x.promoted - or x.promote_until and x.promote_until < datetime.now(g.tz)): - g.log.info('Unpromoting %s' % x._fullname) - unpromote(x) - expired_names.append(x._fullname) - else: - link_names.append(x._fullname) - - set_promoted(link_names) - - return expired_names def get_promoted_slow(): # to be used only by a human at a terminal with g.make_lock(promoted_lock_key): - links = Link._query(Link.c.promoted == True, + links = Link._query(Link.c.promote_status == STATUS.promoted, + Link.c.promoted == True, data = True) - link_names = [ x._fullname for x in links ] + link_names = dict((x._fullname, auction_weight(x)) for x in links) set_promoted(link_names) return link_names -#deprecated -def promote_builder_wrapper(alternative_wrapper): - def wrapper(thing): - if isinstance(thing, Link) and thing.promoted: - w = Wrapped(thing) - w.render_class = PromotedLink - w.rowstyle = 'promoted link' - return w - else: - return alternative_wrapper(thing) - return wrapper +def random_promoted(): + """ + return a list of the currently promoted items, randomly choosing + the order of the list based on the bid-weighing. + """ + bids = get_promoted() + market = sum(bids.values()) + if market: + # get a list of current promotions, sorted by their bid amount + promo_list = bids.keys() + # sort by bids and use the thing_id as the tie breaker (for + # consistent sorting) + promo_list.sort(key = lambda x: (bids[x], x), reverse = True) + if len(bids) > 1: + # pick a number, any number + n = random.uniform(0, 1) + for i, p in enumerate(promo_list): + n -= bids[p] / market + if n < 0: + return promo_list[i:] + promo_list[:i] + return promo_list +def test_random_promoted(n = 1000): + promos = get_promoted() + market = sum(promos.values()) + if market: + res = {} + for i in xrange(n): + key = random_promoted()[0] + res[key] = res.get(key, 0) + 1 + + print "%10s expected actual E/A" % "thing" + print "------------------------------------" + for k, v in promos.iteritems(): + expected = float(v) / market * 100 + actual = float(res.get(k, 0)) / n * 100 + + print "%10s %6.2f%% %6.2f%% %6.2f" % \ + (k, expected, actual, expected / actual if actual else 0) + diff --git a/r2/r2/lib/services.py b/r2/r2/lib/services.py index cf429a1fe..7476d3b39 100644 --- a/r2/r2/lib/services.py +++ b/r2/r2/lib/services.py @@ -36,7 +36,7 @@ class ShellProcess(object): self.proc = subprocess.Popen(cmd, shell = True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - + ntries = int(math.ceil(timeout / sleepcycle)) for n in xrange(ntries): if self.proc.poll() is not None: @@ -45,7 +45,7 @@ class ShellProcess(object): else: print "Process timeout: '%s'" % cmd os.kill(self.proc.pid, signal.SIGTERM) - + self.output, self.error = self.proc.communicate() self.rcode = self.proc.poll() @@ -56,8 +56,8 @@ class ShellProcess(object): def read(self): return self.output - - + + class AppServiceMonitor(Templated): cache_key = "service_datalogger_data_" cache_key_small = "service_datalogger_db_summary_" @@ -76,7 +76,7 @@ class AppServiceMonitor(Templated): """ - def __init__(self, hosts = None): + def __init__(self, hosts = None, queue_length_max = {}): """ hosts is a list of machine hostnames to be tracked. """ @@ -87,7 +87,7 @@ class AppServiceMonitor(Templated): dbase, ip = list(g.to_iter(getattr(g, db + "_db")))[:2] try: name = socket.gethostbyaddr(ip)[0] - + for host in g.monitored_servers: if (name == host or ("." in host and name.endswith("." + host)) or @@ -97,6 +97,13 @@ class AppServiceMonitor(Templated): print "error resolving host: %s" % ip self._db_info = db_info + q_host = g.amqp_host.split(':')[0] + if q_host: + # list of machines that have amqp queues + self._queue_hosts = set([q_host, socket.gethostbyaddr(q_host)[0]]) + # dictionary of max lengths for each queue + self._queue_length_max = queue_length_max + self.hostlogs = [] Templated.__init__(self) @@ -135,7 +142,7 @@ class AppServiceMonitor(Templated): def server_load(self, mach_name): h = self.from_cache(host) return h.load.most_recent() - + def __iter__(self): if not self.hostlogs: self.hostlogs = [self.from_cache(host) for host in self._hosts] @@ -152,13 +159,16 @@ class AppServiceMonitor(Templated): h = HostLogger(host, self) while True: h.monitor(srvname, *a, **kw) - + self.set_cache(h) if loop: time.sleep(loop_time) else: break - + + def is_queue(self, host): + name = socket.gethostbyaddr(host)[0] + return name in self._queue_hosts or host in self._queue_hosts def is_db_machine(self, host): """ @@ -177,7 +187,7 @@ class DataLogger(object): of the interval provided or returns the last element if no interval is provided """ - + def __init__(self, maxlen = 30): self._list = [] self.maxlen = maxlen @@ -186,7 +196,6 @@ class DataLogger(object): self._list.append((value, datetime.utcnow())) if len(self._list) > self.maxlen: self._list = self._list[-self.maxlen:] - def __call__(self, average = None): time = datetime.utcnow() @@ -208,7 +217,6 @@ class DataLogger(object): else: return [0, None] - class Service(object): def __init__(self, name, pid, age): self.name = name @@ -221,6 +229,29 @@ class Service(object): def last_update(self): return max(x.most_recent()[1] for x in [self.mem, self.cpu]) +class AMQueueP(object): + + default_max_queue = 1000 + + def __init__(self, max_lengths = {}): + self.queues = {} + self.max_lengths = max_lengths + + def track(self, cmd = "rabbitmqctl"): + for line in ShellProcess("%s list_queues" % cmd): + try: + name, length = line.split('\t') + length = int(length.strip(' \n')) + self.queues.setdefault(name, DataLogger()).add(length) + except ValueError: + continue + + def max_length(self, name): + return self.max_lengths.get(name, self.default_max_queue) + + def __iter__(self): + for x in sorted(self.queues.keys()): + yield (x, self.queues[x]) class Database(object): @@ -277,12 +308,14 @@ class HostLogger(object): self.load = DataLogger() self.services = {} db_info = master.is_db_machine(host) + is_queue = master.is_queue(host) self.ini_db_names = db_info.keys() self.db_names = set(name for name, ip in db_info.itervalues()) self.db_ips = set(ip for name, ip in db_info.itervalues()) - + self.database = Database() if self.db_names else None + self.queue = AMQueueP(master._queue_length_max) if is_queue else None self.ncpu = 0 try: @@ -320,7 +353,8 @@ class HostLogger(object): def monitor(self, srvname, - srv_params = {}, top_params = {}, db_params = {}): + srv_params = {}, top_params = {}, db_params = {}, + queue_params = {}): # (re)populate the service listing if srvname: for name, status, pid, t in supervise_list(**srv_params): @@ -338,6 +372,9 @@ class HostLogger(object): self.database.track(**check_database(self.db_names, **db_params)) + if self.queue: + self.queue.track(**queue_params) + foo = ShellProcess('/usr/bin/env uptime').read() foo = foo.split("load average")[1].split(':')[1].strip(' ') self.load.add(float(foo.split(' ')[0].strip(','))) @@ -542,6 +579,4 @@ def monitor_cache_lifetime(minutes, retest = 10, ntest = -1, if [] in keys: keys = filter(None, keys) ntest -= 1 - - - + diff --git a/r2/r2/lib/solrsearch.py b/r2/r2/lib/solrsearch.py index a6f0d750b..84d2bbe54 100644 --- a/r2/r2/lib/solrsearch.py +++ b/r2/r2/lib/solrsearch.py @@ -21,25 +21,28 @@ ################################################################################ """ Module for communication reddit-level communication with - Solr. Contains functions for indexing (`reindex_all`, `changed`) + Solr. Contains functions for indexing (`reindex_all`, `run_changed`) and searching (`search_things`). Uses pysolr (placed in r2.lib) for lower-level communication with Solr """ from __future__ import with_statement -from r2.models import * -from r2.lib.contrib import pysolr -from r2.lib.contrib.pysolr import SolrError -from r2.lib.utils import timeago, set_emptying_cache, IteratorChunker -from r2.lib.utils import psave, pload, unicode_safe, tup -from r2.lib.cache import SelfEmptyingCache from Queue import Queue from threading import Thread import time from datetime import datetime, date from time import strftime -from pylons import g,config + +from pylons import g, config + +from r2.models import * +from r2.lib.contrib import pysolr +from r2.lib.contrib.pysolr import SolrError +from r2.lib.utils import timeago +from r2.lib.utils import unicode_safe, tup +from r2.lib.cache import SelfEmptyingCache +from r2.lib import amqp ## Changes to the list of searchable languages will require changes to ## Solr's configuration (specifically, the fields that are searched) @@ -49,7 +52,8 @@ searchable_langs = set(['dk','nl','en','fi','fr','de','it','no','nn','pt', ## Adding types is a matter of adding the class to indexed_types here, ## adding the fields from that type to search_fields below, and adding ## those fields to Solr's configuration -indexed_types = (Subreddit, Link) +indexed_types = (Subreddit, Link) + class Field(object): """ @@ -402,42 +406,6 @@ def reindex_all(types = None, delete_all_first=False): q.put(e,timeout=30) raise e -def changed(commit=True,optimize=False,delete_old=True): - """ - Run by `cron` (through `paster run`) on a schedule to update - all Things that have been created or have changed since the - last run. Things add themselves to a `thing_changes` table, - which we read, find the Things, tokenise, and re-submit them - to Solr - """ - set_emptying_cache() - with SolrConnection(commit=commit,optimize=optimize) as s: - changes = thing_changes.get_changed() - if changes: - max_date = max(x[1] for x in changes) - changed = IteratorChunker(x[0] for x in changes) - - while not changed.done: - chunk = changed.next_chunk(200) - - # chunk =:= [(Fullname,Date) | ...] - chunk = Thing._by_fullname(chunk, - data=True, return_dict=False) - chunk = [x for x in chunk if not x._spam and not x._deleted] - to_delete = [x for x in chunk if x._spam or x._deleted] - - # note: anything marked as spam or deleted is not - # updated in the search database. Since these are - # filtered out in the UI, that's probably fine. - if len(chunk) > 0: - chunk = tokenize_things(chunk) - s.add(chunk) - - for i in to_delete: - s.delete(id=i._fullname) - - if delete_old: - thing_changes.clear_changes(max_date = max_date) def combine_searchterms(terms): """ @@ -728,3 +696,38 @@ def get_after(fullnames, fullname, num): return fullnames[i+1:i+num+1] else: return fullnames[:num] + + +def run_commit(optimize=False): + with SolrConnection(commit=True, optimize=optimize) as s: + pass + + +def run_changed(drain=False): + """ + Run by `cron` (through `paster run`) on a schedule to update + all Things that have been created or have changed since the + last run. Note: unlike many queue-using functions, this one is + run from cron and totally drains the queue before terminating + """ + + def _run_changed(msgs): + print "changed: Processing %d items" % len(msgs) + + fullnames = set([x.body for x in msgs]) + things = Thing._by_fullname(fullnames, data=True, return_dict=False) + things = [x for x in things if isinstance(x, indexed_types)] + + update_things = [x for x in things if not x._spam and not x._deleted] + delete_things = [x for x in things if x._spam or x._deleted] + + with SolrConnection() as s: + if update_things: + tokenized = tokenize_things(update_things) + s.add(tokenized) + if delete_things: + for i in delete_things: + s.delete(id=i._fullname) + + amqp.handle_items('searchchanges_q', _run_changed, limit=1000, + drain=drain) diff --git a/r2/r2/lib/spreadshirt.py b/r2/r2/lib/spreadshirt.py index 2687dfa4c..946c4c0a3 100644 --- a/r2/r2/lib/spreadshirt.py +++ b/r2/r2/lib/spreadshirt.py @@ -125,7 +125,7 @@ class ShirtPage(LinkInfoPage): ShirtPane(self.link))) class ShirtPane(Templated): - default_color = "white" + default_color = "black" default_size = "large" default_style = "men" diff --git a/r2/r2/lib/strings.py b/r2/r2/lib/strings.py index 8e4046136..d17a48a9d 100644 --- a/r2/r2/lib/strings.py +++ b/r2/r2/lib/strings.py @@ -30,7 +30,7 @@ hooks to the UI are the same. import helpers as h from pylons import g from pylons.i18n import _, ungettext -import random +import random, locale __all__ = ['StringHandler', 'strings', 'PluralManager', 'plurals', 'Score', 'rand_strings'] @@ -58,7 +58,7 @@ string_dict = dict( float_label = _("%(num)5.3f %(thing)s"), # this is for Japanese which treats people counds differently - person_label = _("%(num)d %(persons)s"), + person_label = _('%(num)s %(persons)s'), firsttext = _("reddit is a source for what's new and popular online. vote on links that you like or dislike and help decide what's popular, or submit your own!"), @@ -79,14 +79,16 @@ string_dict = dict( friend = None, moderator = _("you have been added as a moderator to [%(title)s](%(url)s)."), contributor = _("you have been added as a contributor to [%(title)s](%(url)s)."), - banned = _("you have been banned from posting to [%(title)s](%(url)s).") + banned = _("you have been banned from posting to [%(title)s](%(url)s)."), + traffic = _('you have been added to the list of users able to see [traffic for the sponsoted link "%(title)s"](%(traffic_url)s).') ), subj_add_friend = dict( friend = None, moderator = _("you are a moderator"), contributor = _("you are a contributor"), - banned = _("you've been banned") + banned = _("you've been banned"), + traffic = _("you can view traffic on a promoted link") ), sr_messages = dict( @@ -96,7 +98,7 @@ string_dict = dict( moderator = _('below are the reddits that you have moderator access to.') ), - sr_subscribe = _('click the `add` or `remove` buttons to choose which reddits appear on your front page.'), + sr_subscribe = _('click the `+frontpage` or `-frontpage` buttons to choose which reddits appear on your front page.'), searching_a_reddit = _('you\'re searching within the [%(reddit_name)s](%(reddit_link)s) reddit. '+ 'you can also search within [all reddits](%(all_reddits_link)s)'), @@ -125,6 +127,9 @@ string_dict = dict( submit_link = _("""You are submitting a link. The key to a successful submission is interesting content and a descriptive title."""), submit_text = _("""You are submitting a text-based post. Speak your mind. A title is required, but expanding further in the text field is not. Beginning your title with "vote up if" is violation of intergalactic law."""), iphone_first = _("You should consider using [reddit's free iphone app](http://itunes.com/apps/iredditfree)."), + verify_email = _("we're going to need to verify your email address for you to proceed."), + email_verified = _("your email address has been verfied"), + email_verify_failed = _("Verification failed. Please try that again"), ) class StringHandler(object): @@ -194,6 +199,7 @@ plurals = PluralManager([P_("comment", "comments"), P_("subreddit", "subreddits"), # people + P_("reader", "readers"), P_("subscriber", "subscribers"), P_("contributor", "contributors"), P_("moderator", "moderators"), @@ -225,10 +231,19 @@ class Score(object): return strings.points_label % dict(num=max(x,0), point=plurals.N_points(x)) + @staticmethod + def _people(x, label): + return strings.person_label % \ + dict(num = locale.format("%d", x, True), + persons = label(x)) + @staticmethod def subscribers(x): - return strings.person_label % \ - dict(num = x, persons = plurals.N_subscribers(x)) + return Score._people(x, plurals.N_subscribers) + + @staticmethod + def readers(x): + return Score._people(x, plurals.N_readers) @staticmethod def none(x): diff --git a/r2/r2/lib/template_helpers.py b/r2/r2/lib/template_helpers.py index ce3d44e6e..4c9cf9e1a 100644 --- a/r2/r2/lib/template_helpers.py +++ b/r2/r2/lib/template_helpers.py @@ -76,14 +76,6 @@ def class_dict(): res = ', '.join(classes) return unsafe('{ %s }' % res) -def path_info(): - loc = dict(path = request.path, - params = dict(request.get)) - - return unsafe(simplejson.dumps(loc)) - - - def replace_render(listing, item, render_func): def _replace_render(style = None, display = True): """ @@ -143,25 +135,31 @@ def replace_render(listing, item, render_func): com_cls = 'comments' replacements['numcomments'] = com_label replacements['commentcls'] = com_cls - + replacements['display'] = "" if display else "style='display:none'" - + if hasattr(item, "render_score"): # replace the score stub (replacements['scoredislikes'], replacements['scoreunvoted'], replacements['scorelikes']) = item.render_score - + # compute the timesince here so we don't end up caching it if hasattr(item, "_date"): - replacements['timesince'] = timesince(item._date) + if hasattr(item, "promoted") and item.promoted is not None: + from r2.lib import promote + # promoted links are special in their date handling + replacements['timesince'] = timesince(item._date - + promote.timezone_offset) + else: + replacements['timesince'] = timesince(item._date) renderer = render_func or item.render res = renderer(style = style, **replacements) if isinstance(res, (str, unicode)): return unsafe(res) return res - + return _replace_render def get_domain(cname = False, subreddit = True, no_www = False): @@ -186,7 +184,7 @@ def get_domain(cname = False, subreddit = True, no_www = False): """ # locally cache these lookups as this gets run in a loop in add_props domain = g.domain - domain_prefix = g.domain_prefix + domain_prefix = c.domain_prefix site = c.site ccname = c.cname if not no_www and domain_prefix: @@ -308,19 +306,51 @@ def panel_size(state): "the frame.cols of the reddit-toolbar's inner frame" return '400px, 100%' if state =='expanded' else '0px, 100%x' -def find_author_class(thing, attribs, gray): - #assume attribs is sorted - author = thing.author - author_cls = "author" +# Appends to the list "attrs" a tuple of: +# +def add_attr(attrs, code, label=None, link=None): + from r2.lib.template_helpers import static - extra_class = '' - attribs.sort() + img = None - if gray: - author_cls += " gray" + if code == 'F': + priority = 1 + cssclass = 'friend' + if not label: + label = _('friend') + if not link: + link = '/prefs/friends' + elif code == 'S': + priority = 2 + cssclass = 'submitter' + if not label: + label = _('submitter') + if not link: + raise ValueError ("Need a link") + elif code == 'M': + priority = 3 + cssclass = 'moderator' + if not label: + raise ValueError ("Need a label") + if not link: + raise ValueError ("Need a link") + elif code == 'A': + priority = 4 + cssclass = 'admin' + if not label: + label = _('reddit admin, speaking officially') + if not link: + link = '/help/faq#Whomadereddit' + elif code == 'trophy': + img = (static('award.png'), '!', 11, 8) + priority = 99 + cssclass = 'recent-trophywinner' + if not label: + raise ValueError ("Need a label") + if not link: + raise ValueError ("Need a link") + else: + raise ValueError ("Got weird code [%s]" % code) - for priority, abbv, css_class, label, attr_link in attribs: - author_cls += " " + css_class - - - return author_cls + attrs.append( (priority, code, cssclass, label, link, img) ) diff --git a/r2/r2/lib/tracking.py b/r2/r2/lib/tracking.py index 9fc6b5a25..02de1b006 100644 --- a/r2/r2/lib/tracking.py +++ b/r2/r2/lib/tracking.py @@ -140,7 +140,7 @@ class UserInfo(Info): self.site = safe_str(c.site.name if c.site else '') self.lang = safe_str(c.lang if c.lang else '') self.cname = safe_str(c.cname) - + class PromotedLinkInfo(Info): _tracked = [] tracker_url = g.adtracker_url @@ -174,7 +174,17 @@ class PromotedLinkClickInfo(PromotedLinkInfo): def tracking_url(self): s = (PromotedLinkInfo.tracking_url(self) + '&url=' + self.dest) return s - + +class AdframeInfo(PromotedLinkInfo): + tracker_url = g.adframetracker_url + + @classmethod + def make_hash(cls, ip, fullname): + return sha.new("%s%s" % (fullname, + g.tracking_secret)).hexdigest() + + + def benchmark(n = 10000): """on my humble desktop machine, this gives ~150 microseconds per gen_url""" import time diff --git a/r2/r2/lib/traffic.py b/r2/r2/lib/traffic.py index 71bd5477b..46efd296f 100644 --- a/r2/r2/lib/traffic.py +++ b/r2/r2/lib/traffic.py @@ -61,9 +61,17 @@ def load_traffic_uncached(interval, what, iden, return [] @memoize("cached_traffic", time = 60) -def load_traffic(interval, what, iden, +def load_traffic(interval, what, iden = '', start_time = None, stop_time = None, npoints = None): + """ + interval = (hour, day, month) + + what = (reddit, lang, thing, promos) + + iden is the specific thing (reddit name, language name, thing + fullname) that one is seeking traffic for. + """ res = load_traffic_uncached(interval, what, iden, start_time = start_time, stop_time = stop_time, npoints = npoints) diff --git a/r2/r2/lib/translation.py b/r2/r2/lib/translation.py index 5d104d51f..204534159 100644 --- a/r2/r2/lib/translation.py +++ b/r2/r2/lib/translation.py @@ -387,11 +387,10 @@ class Translator(LoggedSlots): message = msg, locale = self.locale) key = ts.md5 - if self.enabled.has_key(key): - ts.enabled = self.enabled[key] - if ts.enabled: - indx += 1 - ts.index = indx + if not self.enabled.has_key(key) or self.enabled[key]: + indx += 1 + ts.index = indx + while msgstr.match(line): r, translation, line = get_next_str_block(line, handle) ts.add(translation) @@ -455,6 +454,7 @@ class Translator(LoggedSlots): out_file = file + ".mo" cmd = 'msgfmt -o "%s" "%s"' % (out_file, file) + print cmd with os.popen(cmd) as handle: x = handle.read() if include_index: diff --git a/r2/r2/lib/user_stats.py b/r2/r2/lib/user_stats.py deleted file mode 100644 index fed56b1ec..000000000 --- a/r2/r2/lib/user_stats.py +++ /dev/null @@ -1,72 +0,0 @@ -# The contents of this file are subject to the Common Public Attribution -# License Version 1.0. (the "License"); you may not use this file except in -# compliance with the License. You may obtain a copy of the License at -# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public -# License Version 1.1, but Sections 14 and 15 have been added to cover use of -# software over a computer network and provide for limited attribution for the -# Original Developer. In addition, Exhibit A has been modified to be consistent -# with Exhibit B. -# -# Software distributed under the License is distributed on an "AS IS" basis, -# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -# the specific language governing rights and limitations under the License. -# -# The Original Code is Reddit. -# -# The Original Developer is the Initial Developer. The Initial Developer of the -# Original Code is CondeNet, Inc. -# -# All portions of the code written by CondeNet are Copyright (c) 2006-2009 -# CondeNet, Inc. All Rights Reserved. -################################################################################ -import sqlalchemy as sa -from r2.models import Account, Vote, Link -from r2.lib.db import tdb_sql as tdb -from r2.lib import utils - -from pylons import g -cache = g.cache - -def top_users(): - tt, dt = tdb.get_thing_table(Account._type_id) - - karma = dt.alias() - - s = sa.select([tt.c.thing_id], - sa.and_(tt.c.spam == False, - tt.c.deleted == False, - karma.c.thing_id == tt.c.thing_id, - karma.c.key == 'link_karma'), - order_by = sa.desc(sa.cast(karma.c.value, sa.Integer)), - limit = 10) - rows = s.execute().fetchall() - return [r.thing_id for r in rows] - -def top_user_change(period = '1 day'): - rel = Vote.rel(Account, Link) - rt, account, link, dt = tdb.get_rel_table(rel._type_id) - - author = dt.alias() - - date = utils.timeago(period) - - s = sa.select([author.c.value, sa.func.sum(sa.cast(rt.c.name, sa.Integer))], - sa.and_(rt.c.date > date, - author.c.thing_id == rt.c.rel_id, - author.c.key == 'author_id'), - group_by = author.c.value, - order_by = sa.desc(sa.func.sum(sa.cast(rt.c.name, sa.Integer))), - limit = 10) - - rows = s.execute().fetchall() - - return [(int(r.value), r.sum) for r in rows] - -def calc_stats(): - top = top_users() - top_day = top_user_change('1 day') - top_week = top_user_change('1 week') - return (top, top_day, top_week) - -def set_stats(): - cache.set('stats', calc_stats()) diff --git a/r2/r2/lib/utils/utils.py b/r2/r2/lib/utils/utils.py index c6c4b322e..625467633 100644 --- a/r2/r2/lib/utils/utils.py +++ b/r2/r2/lib/utils/utils.py @@ -23,6 +23,7 @@ from urllib import unquote_plus, quote_plus, urlopen, urlencode from urlparse import urlparse, urlunparse from threading import local, Thread import Queue +import signal from copy import deepcopy import cPickle as pickle import re, datetime, math, random, string, sha, os @@ -199,6 +200,17 @@ def strips(text, remove): """ return rstrips(lstrips(text, remove), remove) +class Enum(Storage): + def __init__(self, *a): + self.name = tuple(a) + Storage.__init__(self, ((e, i) for i, e in enumerate(a))) + def __contains__(self, item): + if isinstance(item, int): + return item in self.values() + else: + return Storage.__contains__(self, item) + + class Results(): def __init__(self, sa_ResultProxy, build_fn, do_batch=False): self.rp = sa_ResultProxy @@ -887,7 +899,7 @@ def set_emptying_cache(): from r2.lib.cache import SelfEmptyingCache g.cache.caches = [SelfEmptyingCache(),] + list(g.cache.caches[1:]) -def find_recent_broken_things(from_time = None, delete = False): +def find_recent_broken_things(from_time = None, to_time = None, delete = False): """ Occasionally (usually during app-server crashes), Things will be partially written out to the database. Things missing data @@ -896,11 +908,10 @@ def find_recent_broken_things(from_time = None, delete = False): them as appropriate. """ from r2.models import Link,Comment + from pylons import g - if not from_time: - from_time = timeago("1 hour") - - to_time = timeago("60 seconds") + from_time = from_time or timeago('1 hour') + to_time = to_time or datetime.now(g.tz) for (cls,attrs) in ((Link,('author_id','sr_id')), (Comment,('author_id','sr_id','body','link_id'))): @@ -921,7 +932,6 @@ def find_broken_things(cls,attrs,from_time,to_time,delete = False): getattr(t,a) except AttributeError: # that failed; let's explicitly load it, and try again - print "Reloading %s" % t._fullname t._load() try: getattr(t,a) @@ -943,37 +953,23 @@ def lineno(): import inspect print "%s\t%s" % (datetime.now(),inspect.currentframe().f_back.f_lineno) -class IteratorChunker(object): - def __init__(self,it): - self.it = it - self.done=False - - def next_chunk(self,size): - chunk = [] - if not self.done: - try: - for i in xrange(size): - chunk.append(self.it.next()) - except StopIteration: - self.done=True - return chunk - def IteratorFilter(iterator, fn): for x in iterator: if fn(x): yield x -def UniqueIterator(iterator): +def UniqueIterator(iterator, key = lambda x: x): """ Takes an iterator and returns an iterator that returns only the first occurence of each entry """ so_far = set() def no_dups(x): - if x in so_far: + k = key(x) + if k in so_far: return False else: - so_far.add(x) + so_far.add(k) return True return IteratorFilter(iterator, no_dups) @@ -1087,7 +1083,7 @@ def link_from_url(path, filter_spam = False, multiple = True): elif a.sr_id not in subs and b.sr_id in subs: return 1 else: - return cmp(a._hot, b._hot) + return cmp(b._hot, a._hot) links = sorted(links, cmp = cmp_links) # among those, show them the hottest one @@ -1106,3 +1102,62 @@ def link_duplicates(article): return duplicates +class TimeoutFunctionException(Exception): + pass + +class TimeoutFunction: + """Force an operation to timeout after N seconds. Works with POSIX + signals, so it's not safe to use in a multi-treaded environment""" + def __init__(self, function, timeout): + self.timeout = timeout + self.function = function + + def handle_timeout(self, signum, frame): + raise TimeoutFunctionException() + + def __call__(self, *args): + # can only be called from the main thread + old = signal.signal(signal.SIGALRM, self.handle_timeout) + signal.alarm(self.timeout) + try: + result = self.function(*args) + finally: + signal.alarm(0) + signal.signal(signal.SIGALRM, old) + return result + +def make_offset_date(start_date, interval, future = True, + business_days = False): + """ + Generates a date in the future or past "interval" days from start_date. + + Can optionally give weekends no weight in the calculation if + "business_days" is set to true. + """ + if interval is not None: + interval = int(interval) + if business_days: + weeks = interval / 7 + dow = start_date.weekday() + if future: + future_dow = (dow + interval) % 7 + if dow > future_dow or future_dow > 4: + weeks += 1 + else: + future_dow = (dow - interval) % 7 + if dow < future_dow or future_dow > 4: + weeks += 1 + interval += 2 * weeks; + if future: + return start_date + timedelta(interval) + return start_date - timedelta(interval) + return start_date + +def to_csv(table): + # commas and linebreaks must result in a quoted string + def quote_commas(x): + if ',' in x or '\n' in x: + return u'"%s"' % x.replace('"', '""') + return x + return u"\n".join(u','.join(quote_commas(y) for y in x) + for x in table) diff --git a/r2/r2/lib/workqueue.py b/r2/r2/lib/workqueue.py index 3d6eb8b5f..65b6454d6 100644 --- a/r2/r2/lib/workqueue.py +++ b/r2/r2/lib/workqueue.py @@ -28,9 +28,11 @@ import time log = g.log + class WorkQueue(object): """A WorkQueue is a queue that takes a number of functions and runs them in parallel""" + global_env = g._current_obj() def __init__(self, jobs = [], num_workers = 5, timeout = None): """Creates a WorkQueue that will process jobs with num_workers @@ -54,7 +56,7 @@ class WorkQueue(object): for worker, start_time in self.workers.items(): if (not worker.isAlive() or self.timeout - and datetime.now() - start_time > self.timeout): + and datetime.now() - start_time > self.timeout): self.work_count.get_nowait() self.jobs.task_done() @@ -62,13 +64,23 @@ class WorkQueue(object): time.sleep(1) + def _init_thread(self, job, global_env): + # make sure that pylons.g is available for the worker thread + g._push_object(global_env) + try: + job() + finally: + # free it up + g._pop_object() + def run(self): """The main thread for the queue. Pull a job off the job queue and create a thread for it.""" while True: job = self.jobs.get() - work_thread = Thread(target = job) + work_thread = Thread(target = self._init_thread, + args=(job, self.global_env)) work_thread.setDaemon(True) self.work_count.put(True) self.workers[work_thread] = datetime.now() @@ -117,4 +129,3 @@ def test(): #q.wait() print 'DONE' - diff --git a/r2/r2/lib/wrapped.py b/r2/r2/lib/wrapped.py index e555bc49d..1683c2f51 100644 --- a/r2/r2/lib/wrapped.py +++ b/r2/r2/lib/wrapped.py @@ -147,7 +147,7 @@ class Templated(object): style, cache = not debug) except AttributeError: raise NoTemplateFound, (repr(self), style) - + return template def cache_key(self, *a): @@ -209,7 +209,6 @@ class Templated(object): """ from pylons import c, g style = style or c.render_style or 'html' - # prepare (and store) the list of cachable items. primary = False if not isinstance(c.render_tracker, dict): @@ -385,7 +384,7 @@ class CachedTemplate(Templated): # these values are needed to render any link on the site, and # a menu is just a set of links, so we best cache against # them. - keys = [c.user_is_loggedin, c.user_is_admin, + keys = [c.user_is_loggedin, c.user_is_admin, c.domain_prefix, c.render_style, c.cname, c.lang, c.site.path, template_hash] keys = [make_cachable(x, *a) for x in keys] diff --git a/r2/r2/models/__init__.py b/r2/r2/models/__init__.py index d3b2cab37..925789ad2 100644 --- a/r2/r2/models/__init__.py +++ b/r2/r2/models/__init__.py @@ -26,6 +26,8 @@ from builder import * from vote import * from report import * from subreddit import * +from award import * +from bidding import * from mail_queue import Email, has_opted_out, opt_count from admintools import * import thing_changes diff --git a/r2/r2/models/account.py b/r2/r2/models/account.py index 24805109e..8638ea350 100644 --- a/r2/r2/models/account.py +++ b/r2/r2/models/account.py @@ -23,11 +23,12 @@ from r2.lib.db.thing import Thing, Relation, NotFound from r2.lib.db.operators import lower from r2.lib.db.userrel import UserRel from r2.lib.memoize import memoize -from r2.lib.utils import modhash, valid_hash, randstr +from r2.lib.utils import modhash, valid_hash, randstr from pylons import g import time, sha from copy import copy +from datetime import datetime, timedelta class AccountExists(Exception): pass @@ -58,12 +59,17 @@ class Account(Thing): report_made = 0, report_correct = 0, report_ignored = 0, + cup_date = None, spammer = 0, sort_options = {}, has_subscribed = False, pref_media = 'subreddit', share = {}, wiki_override = None, + email = "", + email_verified = None, + ignorereports = False, + pref_show_promote = None, ) def karma(self, kind, sr = None): @@ -143,7 +149,22 @@ class Account(Thing): self._t.get('comment_karma', 0))) return karmas - + + def update_last_visit(self, current_time): + from admintools import apply_updates + + apply_updates(self) + + prev_visit = getattr(self, 'last_visit', None) + + if prev_visit and current_time - prev_visit < timedelta(0, 3600): + return + + g.log.debug ("Updating last visit for %s" % self.name) + self.last_visit = current_time + + self._commit() + def make_cookie(self, timestr = None, admin = False): if not self._loaded: self._load() @@ -186,6 +207,14 @@ class Account(Thing): else: raise NotFound, 'Account %s' % name + # Admins only, since it's not memoized + @classmethod + def _by_name_multiple(cls, name): + q = cls._query(lower(Account.c.name) == name.lower(), + Account.c._spam == (True, False), + Account.c._deleted == (True, False)) + return list(q) + @property def friends(self): return self.friend_ids() @@ -235,9 +264,30 @@ class Account(Thing): share['recent'] = emails self.share = share - - - + + def extend_cup(self, new_expiration): + if self.cup_date and self.cup_date > new_expiration: + return + self.cup_date = new_expiration + self._commit() + + def remove_cup(self): + if not self.cup_date: + return + self.cup_date = None + self._commit() + + def should_show_cup(self): + # FIX ME. + # this is being called inside builder (Bad #1) in the + # listing loop. On machines that are not allowed to write to + # the db, this generates an exception (Bad #2) on every + # listing page with users with cups. + return False # temporarily disable cups + if self.cup_date and self.cup_date < datetime.now(g.tz): + self.cup_date = None + self._commit() + return self.cup_date class FakeAccount(Account): _nodb = True @@ -314,6 +364,7 @@ def register(name, password): return a class Friend(Relation(Account, Account)): pass + Account.__bases__ += (UserRel('friend', Friend, disable_reverse_ids_fn = True),) class DeletedUser(FakeAccount): diff --git a/r2/r2/models/admintools.py b/r2/r2/models/admintools.py index 60b8cd66e..9a939afdb 100644 --- a/r2/r2/models/admintools.py +++ b/r2/r2/models/admintools.py @@ -20,9 +20,9 @@ # CondeNet, Inc. All Rights Reserved. ################################################################################ from r2.lib.utils import tup +from r2.lib.filters import websafe from r2.models import Report, Account from r2.models.thing_changes import changed -from r2.lib.db import queries from pylons import g @@ -30,26 +30,41 @@ from datetime import datetime from copy import copy class AdminTools(object): + def spam(self, things, auto, moderator_banned, banner, date = None, **kw): + from r2.lib.db import queries + + things = [x for x in tup(things) if not x._spam] Report.accept(things, True) - things = [ x for x in tup(things) if not x._spam ] for t in things: t._spam = True ban_info = copy(getattr(t, 'ban_info', {})) ban_info.update(auto = auto, moderator_banned = moderator_banned, - banner = banner, banned_at = date or datetime.now(g.tz), **kw) + + if isinstance(banner, dict): + ban_info['banner'] = banner[t._fullname] + else: + ban_info['banner'] = banner + t.ban_info = ban_info t._commit() changed(t) - self.author_spammer(things, True) + + + if not auto: + self.author_spammer(things, True) + self.set_last_sr_ban(things) + queries.ban(things) def unspam(self, things, unbanner = None): + from r2.lib.db import queries + + things = [x for x in tup(things) if x._spam] Report.accept(things, False) - things = [ x for x in tup(things) if x._spam ] for t in things: ban_info = copy(getattr(t, 'ban_info', {})) ban_info['unbanned_at'] = datetime.now(g.tz) @@ -59,7 +74,11 @@ class AdminTools(object): t._spam = False t._commit() changed(t) + + # auto is always False for unbans self.author_spammer(things, False) + self.set_last_sr_ban(things) + queries.unban(things) def author_spammer(self, things, spam): @@ -77,6 +96,24 @@ class AdminTools(object): author = authors[aid] author._incr('spammer', len(author_things) if spam else -len(author_things)) + def set_last_sr_ban(self, things): + by_srid = {} + for thing in things: + if hasattr(thing, 'sr_id'): + by_srid.setdefault(thing.sr_id, []).append(thing) + + if by_srid: + srs = Subreddit._byID(by_srid.keys(), data=True, return_dict=True) + for sr_id, sr_things in by_srid.iteritems(): + sr = srs[sr_id] + + sr.last_mod_action = datetime.now(g.tz) + sr._commit() + sr._incr('mod_actions', len(sr_things)) + + def admin_queues(self, chan, exchange): + pass + admintools = AdminTools() def is_banned_IP(ip): @@ -91,6 +128,9 @@ def valid_thing(v, karma): def valid_user(v, sr, karma): return True +def apply_updates(user): + pass + def update_score(obj, up_change, down_change, new_valid_thing, old_valid_thing): obj._incr('_ups', up_change) obj._incr('_downs', down_change) @@ -99,6 +139,9 @@ def compute_votes(wrapper, item): wrapper.upvotes = item._ups wrapper.downvotes = item._downs +def ip_span(ip): + ip = websafe(ip) + return '' % ip try: from r2admin.models.admintools import * diff --git a/r2/r2/models/award.py b/r2/r2/models/award.py new file mode 100644 index 000000000..0c3680334 --- /dev/null +++ b/r2/r2/models/award.py @@ -0,0 +1,106 @@ +# The contents of this file are subject to the Common Public Attribution +# License Version 1.0. (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +# License Version 1.1, but Sections 14 and 15 have been added to cover use of +# software over a computer network and provide for limited attribution for the +# Original Developer. In addition, Exhibit A has been modified to be consistent +# with Exhibit B. +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +# the specific language governing rights and limitations under the License. +# +# The Original Code is Reddit. +# +# The Original Developer is the Initial Developer. The Initial Developer of the +# Original Code is CondeNet, Inc. +# +# All portions of the code written by CondeNet are Copyright (c) 2006-2008 +# CondeNet, Inc. All Rights Reserved. +################################################################################ +from r2.lib.db.thing import Thing, Relation, NotFound +from r2.lib.db.userrel import UserRel +from r2.lib.db.operators import desc, lower +from r2.lib.db import queries +from r2.lib.memoize import memoize +from r2.models import Account +from pylons import c, g, request + +class Award (Thing): + @classmethod + @memoize('award.all_awards') + def _all_awards_cache(cls): + return [ a._id for a in Award._query(limit=100) ] + + @classmethod + def _all_awards(cls, _update=False): + all = Award._all_awards_cache(_update=_update) + return Award._byID(all, data=True).values() + + @classmethod + def _new(cls, codename, title, imgurl): +# print "Creating new award codename=%s title=%s imgurl=%s" % ( +# codename, title, imgurl) + a = Award(codename=codename, title=title, imgurl=imgurl) + a._commit() + Award._all_awards_cache(_update=True) + + @classmethod + def _by_codename(cls, codename): + q = cls._query(lower(Award.c.codename) == codename.lower()) + q._limit = 1 + award = list(q) + + if award: + return cls._byID(award[0]._id, True) + else: + raise NotFound, 'Award %s' % codename + +class Trophy(Relation(Account, Award)): + @classmethod + def _new(cls, recipient, award, description = None, + url = None, cup_expiration = None): + + # The "name" column of the relation can't be a constant or else a + # given account would not be allowed to win a given award more than + # once. So we're setting it to the string form of the timestamp. + # Still, we won't have that date just yet, so for a moment we're + # setting it to "trophy". + + t = Trophy(recipient, award, "trophy") + + t._name = str(t._date) + + if description: + t.description = description + + if url: + t.url = url + + if cup_expiration: + recipient.extend_cup(cup_expiration) + + t._commit() + Trophy.by_account(recipient, _update=True) + Trophy.by_award(award, _update=True) + + @classmethod + @memoize('trophy.by_account') + def by_account(cls, account): + q = Trophy._query(Trophy.c._thing1_id == account._id, + eager_load = True, thing_data = True, + data = True, + sort = desc('_date')) + q._limit = 50 + return list(q) + + @classmethod + @memoize('trophy.by_award') + def by_award(cls, award): + q = Trophy._query(Trophy.c._thing2_id == award._id, + eager_load = True, thing_data = True, + data = True, + sort = desc('_date')) + q._limit = 500 + return list(q) diff --git a/r2/r2/models/bidding.py b/r2/r2/models/bidding.py new file mode 100644 index 000000000..0498c47c6 --- /dev/null +++ b/r2/r2/models/bidding.py @@ -0,0 +1,442 @@ +# The contents of this file are subject to the Common Public Attribution +# License Version 1.0. (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +# License Version 1.1, but Sections 14 and 15 have been added to cover use of +# software over a computer network and provide for limited attribution for the +# Original Developer. In addition, Exhibit A has been modified to be consistent +# with Exhibit B. +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +# the specific language governing rights and limitations under the License. +# +# The Original Code is Reddit. +# +# The Original Developer is the Initial Developer. The Initial Developer of the +# Original Code is CondeNet, Inc. +# +# All portions of the code written by CondeNet are Copyright (c) 2006-2009 +# CondeNet, Inc. All Rights Reserved. +################################################################################ +from sqlalchemy import Column, String, DateTime, Date, Float, Integer, \ + func as safunc, and_, or_ +from sqlalchemy.exceptions import IntegrityError +from sqlalchemy.schema import PrimaryKeyConstraint +from sqlalchemy.orm import sessionmaker, scoped_session +from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.databases.postgres import PGBigInteger as BigInteger, \ + PGInet as Inet +from sqlalchemy.ext.declarative import declarative_base +from pylons import g +from r2.lib.utils import Enum +from r2.models.account import Account +from r2.lib.db.thing import Thing, NotFound +from pylons import request +from r2.lib.memoize import memoize +import datetime + + +engine = g.dbm.engines['authorize'] +# Allocate a session maker for communicating object changes with the back end +Session = sessionmaker(autocommit = True, autoflush = True, bind = engine) +# allocate a SQLalchemy base class for auto-creation of tables based +# on class fields. +# NB: any class that inherits from this class will result in a table +# being created, and subclassing doesn't work, hence the +# object-inheriting interface classes. +Base = declarative_base(bind = engine) + +class Sessionized(object): + """ + Interface class for wrapping up the "session" in the 0.5 ORM + required for all database communication. This allows subclasses + to have a "query" and "commit" method that doesn't require + managing of the session. + """ + session = Session() + + def __init__(self, *a, **kw): + """ + Common init used by all other classes in this file. Allows + for object-creation based on the __table__ field which is + created by Base (further explained in _disambiguate_args). + """ + for k, v in self._disambiguate_args(None, *a, **kw): + setattr(self, k.name, v) + + @classmethod + def _new(cls, *a, **kw): + """ + Just like __init__, except the new object is committed to the + db before being returned. + """ + obj = cls(*a, **kw) + obj._commit() + return obj + + def _commit(self): + """ + Commits current object to the db. + """ + with self.session.begin(): + self.session.add(self) + + def _delete(self): + """ + Deletes current object from the db. + """ + with self.session.begin(): + self.session.delete(self) + + @classmethod + def query(cls, **kw): + """ + Ubiquitous class-level query function. + """ + q = cls.session.query(cls) + if kw: + q = q.filter_by(**kw) + return q + + @classmethod + def _disambiguate_args(cls, filter_fn, *a, **kw): + """ + Used in _lookup and __init__ to interpret *a as being a list + of args to match columns in the same order as __table__.c + + For example, if a class Foo has fields a and b, this function + allows the two to work identically: + + >>> foo = Foo(a = 'arg1', b = 'arg2') + >>> foo = Foo('arg1', 'arg2') + + Additionally, this function invokes _make_storable on each of + the values in the arg list (including *a as well as + kw.values()) + + """ + args = [] + cols = filter(filter_fn, cls.__table__.c) + for k, v in zip(cols, a): + if not kw.has_key(k.name): + args.append((k, cls._make_storable(v))) + else: + raise TypeError,\ + "got multiple arguments for '%s'" % k.name + + cols = dict((x.name, x) for x in cls.__table__.c) + for k, v in kw.iteritems(): + if cols.has_key(k): + args.append((cols[k], cls._make_storable(v))) + return args + + @classmethod + def _make_storable(self, val): + if isinstance(val, Account): + return val._id + elif isinstance(val, Thing): + return val._fullname + else: + return val + + @classmethod + def _lookup(cls, multiple, *a, **kw): + """ + Generates an executes a query where it matches *a to the + primary keys of the current class's table. + + The primary key nature can be overridden by providing an + explicit list of columns to search. + + This function is only a convenience function, and is called + only by one() and lookup(). + """ + args = cls._disambiguate_args(lambda x: x.primary_key, *a, **kw) + res = cls.query().filter(and_(*[k == v for k, v in args])) + try: + res = res.all() if multiple else res.one() + # res.one() will raise NoResultFound, while all() will + # return an empty list. This will make the response + # uniform + if not res: + raise NoResultFound + except NoResultFound: + raise NotFound, "%s with %s" % \ + (cls.__name__, + ",".join("%s=%s" % x for x in args)) + return res + + @classmethod + def lookup(cls, *a, **kw): + """ + Returns all objects which match the kw list, or primary keys + that match the *a. + """ + return cls._lookup(True, *a, **kw) + + @classmethod + def one(cls, *a, **kw): + """ + Same as lookup, but returns only one argument. + """ + return cls._lookup(False, *a, **kw) + + @classmethod + def add(cls, key, *a): + try: + cls.one(key, *a) + except NotFound: + cls(key, *a)._commit() + + @classmethod + def delete(cls, key, *a): + try: + cls.one(key, *a)._delete() + except NotFound: + pass + + @classmethod + def get(cls, key): + try: + return cls.lookup(key) + except NotFound: + return [] + +class CustomerID(Sessionized, Base): + __tablename__ = "authorize_account_id" + + account_id = Column(BigInteger, primary_key = True, + autoincrement = False) + authorize_id = Column(BigInteger) + + def __repr__(self): + return "" % self.authorize_id + + @classmethod + def set(cls, user, _id): + try: + existing = cls.one(user) + existing.authorize_id = _id + existing._commit() + except NotFound: + cls(user, _id)._commit() + + @classmethod + def get_id(cls, user): + try: + return cls.one(user).authorize_id + except NotFound: + return + +class PayID(Sessionized, Base): + __tablename__ = "authorize_pay_id" + + account_id = Column(BigInteger, primary_key = True, + autoincrement = False) + pay_id = Column(BigInteger, primary_key = True, + autoincrement = False) + + def __repr__(self): + return "<%s(%d)>" % (self.__class__.__name__, self.authorize_id) + + @classmethod + def get_ids(cls, key): + return [int(x.pay_id) for x in cls.get(key)] + +class ShippingAddress(Sessionized, Base): + __tablename__ = "authorize_ship_id" + + account_id = Column(BigInteger, primary_key = True, + autoincrement = False) + ship_id = Column(BigInteger, primary_key = True, + autoincrement = False) + + def __repr__(self): + return "<%s(%d)>" % (self.__class__.__name__, self.authorize_id) + +class Bid(Sessionized, Base): + __tablename__ = "bids" + + STATUS = Enum("AUTH", "CHARGE", "REFUND", "VOID") + + # will be unique from authorize + transaction = Column(BigInteger, primary_key = True, + autoincrement = False) + + # identifying characteristics + account_id = Column(BigInteger, index = True, nullable = False) + pay_id = Column(BigInteger, index = True, nullable = False) + thing_id = Column(BigInteger, index = True, nullable = False) + + # breadcrumbs + ip = Column(Inet) + date = Column(DateTime(timezone = True), default = safunc.now(), + nullable = False) + + # bid information: + bid = Column(Float, nullable = False) + charge = Column(Float) + + status = Column(Integer, nullable = False, + default = STATUS.AUTH) + + + @classmethod + def _new(cls, trans_id, user, pay_id, thing_id, bid): + bid = Bid(trans_id, user, pay_id, + thing_id, getattr(request, 'ip', '0.0.0.0'), bid = bid) + bid._commit() + return bid + + def set_status(self, status): + if self.status != status: + self.status = status + self._commit() + + def auth(self): + self.set_status(self.STATUS.AUTH) + + def void(self): + self.set_status(self.STATUS.VOID) + + def charged(self): + self.set_status(self.STATUS.CHARGE) + + def refund(self): + self.set_status(self.STATUS.REFUND) + +class PromoteDates(Sessionized, Base): + __tablename__ = "promote_date" + + thing_name = Column(String, primary_key = True, autoincrement = False) + + account_id = Column(BigInteger, index = True, autoincrement = False) + + start_date = Column(Date(), nullable = False, index = True) + end_date = Column(Date(), nullable = False, index = True) + + actual_start = Column(DateTime(timezone = True), index = True) + actual_end = Column(DateTime(timezone = True), index = True) + + bid = Column(Float) + refund = Column(Float) + + @classmethod + def update(cls, thing, start_date, end_date): + try: + promo = cls.one(thing) + promo.start_date = start_date.date() + promo.end_date = end_date.date() + promo._commit() + except NotFound: + promo = cls._new(thing, thing.author_id, start_date, end_date) + + @classmethod + def update_bid(cls, thing): + bid = thing.promote_bid + refund = 0 + if thing.promote_trans_id < 0: + refund = bid + elif hasattr(thing, "promo_refund"): + refund = thing.promo_refund + promo = cls.one(thing) + promo.bid = bid + promo.refund = refund + promo._commit() + + @classmethod + def log_start(cls, thing): + promo = cls.one(thing) + promo.actual_start = datetime.datetime.now(g.tz) + promo._commit() + cls.update_bid(thing) + + @classmethod + def log_end(cls, thing): + promo = cls.one(thing) + promo.actual_end = datetime.datetime.now(g.tz) + promo._commit() + cls.update_bid(thing) + + @classmethod + def for_date(cls, date): + if isinstance(date, datetime.datetime): + date = date.date() + q = cls.query().filter(and_(cls.start_date <= date, + cls.end_date > date)) + return q.all() + + @classmethod + def for_date_range(cls, start_date, end_date, account_id = None): + if isinstance(start_date, datetime.datetime): + start_date = start_date.date() + if isinstance(end_date, datetime.datetime): + end_date = end_date.date() + # Three cases to be included: + # 1) start date is in the provided interval + start_inside = and_(cls.start_date >= start_date, + cls.start_date < end_date) + # 2) end date is in the provided interval + end_inside = and_(cls.end_date >= start_date, + cls.end_date < end_date) + # 3) interval is a subset of a promoted interval + surrounds = and_(cls.start_date <= start_date, + cls.end_date >= end_date) + + q = cls.query().filter(or_(start_inside, end_inside, surrounds)) + if account_id is not None: + q = q.filter(cls.account_id == account_id) + + return q.all() + + @classmethod + @memoize('promodates.bid_history', time = 10 * 60) + def bid_history(cls, start_date, end_date = None, account_id = None): + + end_date = end_date or datetime.datetime.now(g.tz) + q = cls.for_date_range(start_date, end_date, account_id = account_id) + + d = start_date.date() + end_date = end_date.date() + res = [] + while d < end_date: + bid = 0 + refund = 0 + for i in q: + end = i.actual_end.date() if i.actual_end else i.end_date + start = i.actual_start.date() if i.actual_start else None + if start and start <= d and end > d: + duration = float((end - start).days) + bid += i.bid / duration + refund += i.refund / duration + res.append([d, bid, refund]) + d += datetime.timedelta(1) + return res + + @classmethod + @memoize('promodates.top_promoters', time = 10 * 60) + def top_promoters(cls, start_date, end_date = None): + end_date = end_date or datetime.datetime.now(g.tz) + q = cls.for_date_range(start_date, end_date) + + d = start_date + res = [] + accounts = Account._byID([i.account_id for i in q], + return_dict = True, data = True) + res = {} + for i in q: + if i.bid is not None and i.actual_start is not None: + r = res.setdefault(i.account_id, [0, 0, set()]) + r[0] += i.bid + r[1] += i.refund + r[2].add(i.thing_name) + res = [ ([accounts[k]] + v) for (k, v) in res.iteritems() ] + res.sort(key = lambda x: x[1] - x[2], reverse = True) + + return res + + +# do all the leg work of creating/connecting to tables +Base.metadata.create_all() + diff --git a/r2/r2/models/builder.py b/r2/r2/models/builder.py index bf3f087b9..4bf2585fd 100644 --- a/r2/r2/models/builder.py +++ b/r2/r2/models/builder.py @@ -35,53 +35,15 @@ from r2.lib import utils from r2.lib.db import operators from r2.lib.cache import sgm from r2.lib.comment_tree import link_comments - from copy import deepcopy, copy import time from datetime import datetime,timedelta -from admintools import compute_votes, admintools +from admintools import compute_votes, admintools, ip_span EXTRA_FACTOR = 1.5 MAX_RECURSION = 10 -# Appends to the list "attrs" a tuple of: -# -def add_attr(attrs, code, label=None, link=None): - if code == 'F': - priority = 1 - cssclass = 'friend' - if not label: - label = _('friend') - if not link: - link = '/prefs/friends' - elif code == 'S': - priority = 2 - cssclass = 'submitter' - if not label: - label = _('submitter') - if not link: - raise ValueError ("Need a link") - elif code == 'M': - priority = 3 - cssclass = 'moderator' - if not label: - raise ValueError ("Need a label") - if not link: - raise ValueError ("Need a link") - elif code == 'A': - priority = 4 - cssclass = 'admin' - if not label: - label = _('reddit admin, speaking officially') - if not link: - link = '/help/faq#Whomadereddit' - else: - raise ValueError ("Got weird code [%s]" % code) - - attrs.append( (priority, code, cssclass, label, link) ) - class Builder(object): def __init__(self, wrap = Wrapped, keep_fn = None): self.wrap = wrap @@ -94,11 +56,13 @@ class Builder(object): return item.keep_item(item) def wrap_items(self, items): + from r2.lib.template_helpers import add_attr user = c.user if c.user_is_loggedin else None #get authors #TODO pull the author stuff into add_props for links and #comments and messages? + try: aids = set(l.author_id for l in items) except AttributeError: @@ -151,8 +115,7 @@ class Builder(object): w.author = None w.friend = False - # List of tuples + # List of tuples (see add_attr() for details) w.attribs = [] w.distinguished = None @@ -181,6 +144,13 @@ class Builder(object): getattr(item, "author_id", None) in mods): add_attr(w.attribs, 'M', label=modlabel, link=modlink) + if (g.show_awards and w.author + and w.author.should_show_cup()): + add_attr(w.attribs, 'trophy', label= + _("%(user)s recently won a trophy! click here to see it.") + % {'user':w.author.name}, + link = "/user/%s" % w.author.name) + if hasattr(item, "sr_id"): w.subreddit = subreddits[item.sr_id] @@ -214,6 +184,11 @@ class Builder(object): count += 1 + if c.user_is_admin and getattr(item, 'ip', None): + w.ip_span = ip_span(item.ip) + else: + w.ip_span = "" + # if the user can ban things on a given subreddit, or an # admin, then allow them to see that the item is spam, and # add the other spam-related display attributes @@ -365,12 +340,12 @@ class QueryBuilder(Builder): #skip and count while new_items and (not self.num or num_have < self.num): i = new_items.pop(0) - count = count - 1 if self.reverse else count + 1 if not (self.must_skip(i) or self.skip and not self.keep_item(i)): items.append(i) num_have += 1 - if self.wrap: - i.num = count + if self.wrap: + count = count - 1 if self.reverse else count + 1 + i.num = count last_item = i #unprewrap the last item diff --git a/r2/r2/models/link.py b/r2/r2/models/link.py index 15281620a..21a7dffae 100644 --- a/r2/r2/models/link.py +++ b/r2/r2/models/link.py @@ -48,9 +48,7 @@ class Link(Thing, Printable): media_object = None, has_thumbnail = False, promoted = None, - promoted_subscribersonly = False, - promote_until = None, - promoted_by = None, + pending = False, disable_comments = False, selftext = '', ip = '0.0.0.0') @@ -211,8 +209,16 @@ class Link(Thing, Printable): @staticmethod def wrapped_cache_key(wrapped, style): s = Printable.wrapped_cache_key(wrapped, style) + if wrapped.promoted is not None: + s.extend([getattr(wrapped, "promote_status", -1), + wrapped.disable_comments, + wrapped._date, + wrapped.promote_until, + c.user_is_sponsor, + wrapped.url, repr(wrapped.title)]) if style == "htmllite": - s.append(request.get.has_key('twocolumn')) + s.extend([request.get.has_key('twocolumn'), + c.link_target]) elif style == "xml": s.append(request.GET.has_key("nothumbs")) s.append(getattr(wrapped, 'media_object', {})) @@ -221,7 +227,15 @@ class Link(Thing, Printable): def make_permalink(self, sr, force_domain = False): from r2.lib.template_helpers import get_domain p = "comments/%s/%s/" % (self._id36, title_to_url(self.title)) - if not c.cname and not force_domain: + # promoted links belong to a separate subreddit and shouldn't + # include that in the path + if self.promoted is not None: + if force_domain: + res = "http://%s/%s" % (get_domain(cname = False, + subreddit = False), p) + else: + res = "/%s" % p + elif not c.cname and not force_domain: res = "/r/%s/%s" % (sr.name, p) elif sr != c.site or force_domain: res = "http://%s/%s" % (get_domain(cname = (c.cname and @@ -229,6 +243,11 @@ class Link(Thing, Printable): subreddit = not c.cname), p) else: res = "/%s" % p + + # WARNING: If we ever decide to add any ?foo=bar&blah parameters + # here, Comment.make_permalink will need to be updated or else + # it will fail. + return res def make_permalink_slow(self, force_domain = False): @@ -256,6 +275,7 @@ class Link(Thing, Printable): saved = Link._saved(user, wrapped) if user_is_loggedin else {} hidden = Link._hidden(user, wrapped) if user_is_loggedin else {} + #clicked = Link._clicked(user, wrapped) if user else {} clicked = {} @@ -264,17 +284,18 @@ class Link(Thing, Printable): if not hasattr(item, "score_fmt"): item.score_fmt = Score.number_only item.pref_compress = user.pref_compress - if user.pref_compress: + if user.pref_compress and item.promoted is None: item.render_css_class = "compressed link" item.score_fmt = Score.points - elif pref_media == 'on': + elif pref_media == 'on' and not user.pref_compress: show_media = True elif pref_media == 'subreddit' and item.subreddit.show_media: show_media = True - elif (item.promoted - and item.has_thumbnail - and pref_media != 'off'): - show_media = True + elif item.promoted and item.has_thumbnail: + if user_is_loggedin and item.author_id == user._id: + show_media = True + elif pref_media != 'off' and not user.pref_compress: + show_media = True if not show_media: item.thumbnail = "" @@ -358,7 +379,9 @@ class Link(Thing, Printable): else: item.href_url = item.url - if pref_frame and not item.is_self: + # show the toolbar if the preference is set and the link + # is neither a promoted link nor a self post + if pref_frame and not item.is_self and not item.promoted: item.mousedown_url = item.tblink else: item.mousedown_url = None @@ -370,6 +393,8 @@ class Link(Thing, Printable): item._deleted, item._spam)) + item.is_author = (user == item.author) + # bits that we will render stubs (to make the cached # version more flexible) item.num = CachedVariable("num") @@ -377,7 +402,7 @@ class Link(Thing, Printable): item.commentcls = CachedVariable("commentcls") item.midcolmargin = CachedVariable("midcolmargin") item.comment_label = CachedVariable("numcomments") - + if user_is_loggedin: incr_counts(wrapped) @@ -402,32 +427,21 @@ class PromotedLink(Link): @classmethod def add_props(cls, user, wrapped): + # prevents cyclic dependencies + from r2.lib import promote Link.add_props(user, wrapped) user_is_sponsor = c.user_is_sponsor - try: - if user_is_sponsor: - promoted_by_ids = set(x.promoted_by - for x in wrapped - if hasattr(x,'promoted_by')) - promoted_by_accounts = Account._byID(promoted_by_ids, - data=True) - else: - promoted_by_accounts = {} - - except NotFound: - # since this is just cosmetic, we can skip it altogether - # if one isn't found or is broken - promoted_by_accounts = {} + status_dict = dict((v, k) for k, v in promote.STATUS.iteritems()) for item in wrapped: # these are potentially paid for placement item.nofollow = True item.user_is_sponsor = user_is_sponsor - if item.promoted_by in promoted_by_accounts: - item.promoted_by_name = promoted_by_accounts[item.promoted_by].name + status = getattr(item, "promote_status", -1) + if item.is_author or c.user_is_sponsor: + item.rowstyle = "link " + promote.STATUS.name[status].lower() else: - # keep the template from trying to read it - item.promoted_by = None + item.rowstyle = "link promoted" # Run this last Printable.add_props(user, wrapped) @@ -443,7 +457,7 @@ class Comment(Thing, Printable): def _delete(self): link = Link._byID(self.link_id, data = True) link._incr('num_comments', -1) - + @classmethod def _new(cls, author, link, parent, body, ip): c = Comment(body = body, @@ -462,12 +476,16 @@ class Comment(Thing, Printable): link._incr('num_comments', 1) - inbox_rel = None + to = None if parent: to = Account._byID(parent.author_id) - # only global admins can be message spammed. - if not c._spam or to.name in g.admins: - inbox_rel = Inbox._add(to, c, 'inbox') + elif link.is_self: + to = Account._byID(link.author_id) + + inbox_rel = None + # only global admins can be message spammed. + if to and (not c._spam or to.name in g.admins): + inbox_rel = Inbox._add(to, c, 'inbox') return (c, inbox_rel) @@ -497,16 +515,23 @@ class Comment(Thing, Printable): s.extend([wrapped.body]) return s - def make_permalink(self, link, sr=None): - return link.make_permalink(sr) + self._id36 + def make_permalink(self, link, sr=None, context=None, anchor=False): + url = link.make_permalink(sr) + self._id36 + if context: + url += "?context=%d" % context + if anchor: + url += "#%s" % self._id36 + return url - def make_permalink_slow(self): + def make_permalink_slow(self, context=None, anchor=False): l = Link._byID(self.link_id, data=True) - return self.make_permalink(l, l.subreddit_slow) - + return self.make_permalink(l, l.subreddit_slow, + context=context, anchor=anchor) + @classmethod def add_props(cls, user, wrapped): - from r2.models.builder import add_attr + from r2.lib.template_helpers import add_attr + from r2.lib import promote #fetch parent links @@ -517,11 +542,13 @@ class Comment(Thing, Printable): for cm in wrapped: if not hasattr(cm, 'sr_id'): cm.sr_id = links[cm.link_id].sr_id - + subreddits = Subreddit._byID(set(cm.sr_id for cm in wrapped), data=True,return_dict=False) + can_reply_srs = set(s._id for s in subreddits if s.can_comment(user)) \ if c.user_is_loggedin else set() + can_reply_srs.add(promote.PromoteSR._id) min_score = user.pref_min_comment_score @@ -539,7 +566,6 @@ class Comment(Thing, Printable): if (item.link._score <= 1 or item.score < 3 or item.link._spam or item._spam or item.author._spam): - item.nofollow = True else: item.nofollow = False @@ -651,7 +677,7 @@ class MoreChildren(MoreComments): pass class Message(Thing, Printable): - _defaults = dict(reported = 0,) + _defaults = dict(reported = 0, was_comment = False) _data_int_props = Thing._data_int_props + ('reported', ) cache_ignore = set(["to"]).union(Printable.cache_ignore) @@ -684,6 +710,14 @@ class Message(Thing, Printable): #load the "to" field if required to_ids = set(w.to_id for w in wrapped) tos = Account._byID(to_ids, True) if to_ids else {} + links = Link._byID(set(l.link_id for l in wrapped if l.was_comment), + data = True, + return_dict = True) + subreddits = Subreddit._byID(set(l.sr_id for l in links.values()), + data = True, return_dict = True) + parents = Comment._byID(set(l.parent_id for l in wrapped + if hasattr(l, "parent_id") and l.was_comment), + data = True, return_dict = True) for item in wrapped: item.to = tos[item.to_id] @@ -692,6 +726,23 @@ class Message(Thing, Printable): else: item.new = False item.score_fmt = Score.none + + item.message_style = "" + if item.was_comment: + link = links[item.link_id] + sr = subreddits[link.sr_id] + item.link_title = link.title + item.link_permalink = link.make_permalink(sr) + if hasattr(item, "parent_id"): + item.subject = _('comment reply') + item.message_style = "comment-reply" + parent = parents[item.parent_id] + item.parent = parent._fullname + item.parent_permalink = parent.make_permalink(link, sr) + else: + item.subject = _('post reply') + item.message_style = "post-reply" + # Run this last Printable.add_props(user, wrapped) diff --git a/r2/r2/models/mail_queue.py b/r2/r2/models/mail_queue.py index 4953bb79a..606656b90 100644 --- a/r2/r2/models/mail_queue.py +++ b/r2/r2/models/mail_queue.py @@ -25,13 +25,13 @@ from email.MIMEText import MIMEText import sqlalchemy as sa from sqlalchemy.databases.postgres import PGInet, PGBigInteger -from r2.lib.db.tdb_sql import make_metadata -from r2.models.thing_changes import changed, index_str, create_table -from r2.lib.utils import Storage, timeago +from r2.lib.db.tdb_sql import make_metadata, index_str, create_table +from r2.lib.utils import Storage, timeago, Enum, tup from account import Account from r2.lib.db.thing import Thing from r2.lib.memoize import memoize -from pylons import g +from pylons import g, request +from pylons.i18n import _ def mail_queue(metadata): return sa.Table(g.db_app_name + '_mail_queue', metadata, @@ -129,11 +129,11 @@ class EmailHandler(object): self.queue_table = mail_queue(self.metadata) indices = [index_str(self.queue_table, "date", "date"), index_str(self.queue_table, 'kind', 'kind')] - create_table(self.queue_table, indices, force = force) + create_table(self.queue_table, indices) self.opt_table = opt_out(self.metadata) indices = [index_str(self.opt_table, 'email', 'email')] - create_table(self.opt_table, indices, force = force) + create_table(self.opt_table, indices) self.track_table = sent_mail_table(self.metadata) self.reject_table = sent_mail_table(self.metadata, name = "reject_mail") @@ -148,8 +148,8 @@ class EmailHandler(object): index_str(tab, 'msg_hash', 'msg_hash'), ] - create_table(self.track_table, sent_indices(self.track_table), force = force) - create_table(self.reject_table, sent_indices(self.reject_table), force = force) + create_table(self.track_table, sent_indices(self.track_table)) + create_table(self.reject_table, sent_indices(self.reject_table)) def __repr__(self): return "" @@ -202,15 +202,20 @@ class EmailHandler(object): return res[0][0] if res and res[:1] else None - def add_to_queue(self, user, thing, emails, from_name, fr_addr, date, ip, - kind, body = "", reply_to = ""): + def add_to_queue(self, user, emails, from_name, fr_addr, kind, + date = None, ip = None, + body = "", reply_to = "", thing = None): s = self.queue_table hashes = [] - for email in emails: + if not date: + date = datetime.datetime.now(g.tz) + if not ip: + ip = getattr(request, "ip", "127.0.0.1") + for email in tup(emails): uid = user._id if user else 0 tid = thing._fullname if thing else "" key = sha.new(str((email, from_name, uid, tid, ip, kind, body, - datetime.datetime.now()))).hexdigest() + datetime.datetime.now(g.tz)))).hexdigest() s.insert().values({s.c.to_addr : email, s.c.account_id : uid, s.c.from_name : from_name, @@ -285,8 +290,35 @@ class EmailHandler(object): class Email(object): handler = EmailHandler() - Kind = ["SHARE", "FEEDBACK", "ADVERTISE", "OPTOUT", "OPTIN"] - Kind = Storage((e, i) for i, e in enumerate(Kind)) + Kind = Enum("SHARE", "FEEDBACK", "ADVERTISE", "OPTOUT", "OPTIN", + "VERIFY_EMAIL", "RESET_PASSWORD", + "BID_PROMO", + "ACCEPT_PROMO", + "REJECT_PROMO", + "QUEUED_PROMO", + "LIVE_PROMO", + "FINISHED_PROMO", + "NEW_PROMO", + "HELP_TRANSLATE", + ) + + subjects = { + Kind.SHARE : _("[reddit] %(user)s has shared a link with you"), + Kind.FEEDBACK : _("[feedback] feedback from '%(user)s'"), + Kind.ADVERTISE : _("[ad_inq] feedback from '%(user)s'"), + Kind.OPTOUT : _("[reddit] email removal notice"), + Kind.OPTIN : _("[reddit] email addition notice"), + Kind.RESET_PASSWORD : _("[reddit] reset your password"), + Kind.VERIFY_EMAIL : _("[reddit] verify your email address"), + Kind.BID_PROMO : _("[reddit] your bid has been accepted"), + Kind.ACCEPT_PROMO : _("[reddit] your promotion has been accepted"), + Kind.REJECT_PROMO : _("[reddit] your promotion has been rejected"), + Kind.QUEUED_PROMO : _("[reddit] your promotion has been queued"), + Kind.LIVE_PROMO : _("[reddit] your promotion is now live"), + Kind.FINISHED_PROMO : _("[reddit] your promotion has finished"), + Kind.NEW_PROMO : _("[reddit] your promotion has been created"), + Kind.HELP_TRANSLATE : _("[i18n] translation offer from '%(user)s'"), + } def __init__(self, user, thing, email, from_name, date, ip, banned_ip, kind, msg_hash, body = '', from_addr = '', @@ -302,9 +334,14 @@ class Email(object): self.kind = kind self.sent = False self.body = body - self.subject = '' self.msg_hash = msg_hash self.reply_to = reply_to + self.subject = self.subjects.get(kind, "") + try: + self.subject = self.subject % dict(user = self.from_name()) + except UnicodeDecodeError: + self.subject = self.subject % dict(user = "a user") + def from_name(self): if not self.user: diff --git a/r2/r2/models/populatedb.py b/r2/r2/models/populatedb.py index 0a671e0af..ef04ec09c 100644 --- a/r2/r2/models/populatedb.py +++ b/r2/r2/models/populatedb.py @@ -20,7 +20,6 @@ # CondeNet, Inc. All Rights Reserved. ################################################################################ from r2.models import * -from r2.lib import promote from r2.lib.utils import fetch_things2 import string @@ -62,9 +61,6 @@ def create_links(num): sr = random.choice(subreddits) l = Link._submit(title, url, user, sr, '127.0.0.1') - if random.choice(([False] * 50) + [True]): - promote.promote(l) - def by_url_cache(): q = Link._query(Link.c._spam == (True,False), diff --git a/r2/r2/models/printable.py b/r2/r2/models/printable.py index 7f26e0bc5..722b21fa5 100644 --- a/r2/r2/models/printable.py +++ b/r2/r2/models/printable.py @@ -40,7 +40,7 @@ class Printable(object): 'render_score', 'score', '_score', 'upvotes', '_ups', 'downvotes', '_downs', - 'subreddit_slow', + 'subreddit_slow', '_deleted', '_spam', 'cachable', 'make_permalink', 'permalink', 'timesince', 'votehash' ]) diff --git a/r2/r2/models/subreddit.py b/r2/r2/models/subreddit.py index a4728de07..c987bcc68 100644 --- a/r2/r2/models/subreddit.py +++ b/r2/r2/models/subreddit.py @@ -50,16 +50,24 @@ class Subreddit(Thing, Printable): description = '', allow_top = True, images = {}, + ad_type = None, ad_file = os.path.join(g.static_path, 'ad_default.html'), reported = 0, valid_votes = 0, show_media = False, + css_on_cname = True, domain = None, + mod_actions = 0, + sponsorship_url = None, + sponsorship_img = None, + sponsorship_name = None, ) + _data_int_props = ('mod_actions',) + sr_limit = 50 @classmethod - def _new(self, name, title, author_id, ip, lang = g.lang, type = 'public', + def _new(cls, name, title, author_id, ip, lang = g.lang, type = 'public', over_18 = False, **kw): with g.make_lock('create_sr_' + name.lower()): try: @@ -265,8 +273,9 @@ class Subreddit(Thing, Printable): else: item.subscriber = bool(rels.get((item, user, 'subscriber'))) item.moderator = bool(rels.get((item, user, 'moderator'))) - item.contributor = bool(item.moderator or \ - rels.get((item, user, 'contributor'))) + item.contributor = bool(item.type != 'public' and + (item.moderator or + rels.get((item, user, 'contributor')))) item.score = item._ups # override "voting" score behavior (it will override the use of # item.score in builder.py to be ups-downs) @@ -274,6 +283,12 @@ class Subreddit(Thing, Printable): base_score = item.score - (1 if item.likes else 0) item.voting_score = [(base_score + x - 1) for x in range(3)] item.score_fmt = Score.subscribers + + #will seem less horrible when add_props is in pages.py + from r2.lib.pages import UserText + item.usertext = UserText(item, item.description) + + Printable.add_props(user, wrapped) #TODO: make this work cache_ignore = set(["subscribers"]).union(Printable.cache_ignore) @@ -290,7 +305,7 @@ class Subreddit(Thing, Printable): pop_reddits = Subreddit._query(Subreddit.c.type == ('public', 'restricted'), sort=desc('_downs'), - limit = limit * 1.5 if limit else None, + limit = limit, data = True, read_cache = True, write_cache = True, @@ -301,14 +316,7 @@ class Subreddit(Thing, Printable): if not c.over18: pop_reddits._filter(Subreddit.c.over_18 == False) - # evaluate the query and remove the ones with - # allow_top==False. Note that because this filtering is done - # after the query is run, if there are a lot of top reddits - # with allow_top==False, we may return fewer than `limit` - # results. - srs = filter(lambda sr: sr.allow_top, pop_reddits) - - return srs[:limit] if limit else srs + return list(pop_reddits) @classmethod def default_subreddits(cls, ids = True, limit = g.num_default_reddits): @@ -318,8 +326,26 @@ class Subreddit(Thing, Printable): An optional kw argument 'limit' is defaulted to g.num_default_reddits """ - srs = cls.top_lang_srs(c.content_langs, limit) - return [s._id for s in srs] if ids else srs + + # If we ever have much more than two of these, we should update + # _by_name to support lists of them + auto_srs = [ Subreddit._by_name(n) for n in g.automatic_reddits ] + + srs = cls.top_lang_srs(c.content_langs, limit + len(auto_srs)) + rv = [] + for i, s in enumerate(srs): + if len(rv) >= limit: + break + if s in auto_srs: + continue + rv.append(s) + + rv = auto_srs + rv + + if ids: + return [ sr._id for sr in rv ] + else: + return rv @classmethod @memoize('random_reddits', time = 1800) @@ -516,6 +542,7 @@ class AllSR(FakeSubreddit): title = 'all' def get_links(self, sort, time): + from r2.lib import promote from r2.models import Link from r2.lib.db import queries q = Link._query(sort = queries.db_sort(sort)) diff --git a/r2/r2/models/thing_changes.py b/r2/r2/models/thing_changes.py index ad8184c02..a3647bd76 100644 --- a/r2/r2/models/thing_changes.py +++ b/r2/r2/models/thing_changes.py @@ -21,89 +21,14 @@ ################################################################################ import sqlalchemy as sa -from r2.lib.db.tdb_sql import make_metadata -from r2.lib.utils import worker - from pylons import g -def index_str(table, name, on, where = None): - index_str = 'create index idx_%s_' % name - index_str += table.name - index_str += ' on '+ table.name + ' (%s)' % on - if where: - index_str += ' where %s' % where - return index_str - -def create_table(table, index_commands=None, force = False): - t = table - if g.db_create_tables: - if not t.bind.has_table(t.name) or force: - try: - t.create(checkfirst = False) - except: pass - if index_commands: - for i in index_commands: - try: - t.bind.execute(i) - except: pass +from r2.lib import amqp +from r2.lib.utils import worker -def change_table(metadata): - return sa.Table(g.db_app_name + '_changes', metadata, - sa.Column('fullname', sa.String, nullable=False, - primary_key = True), - sa.Column('thing_type', sa.Integer, nullable=False), - sa.Column('date', - sa.DateTime(timezone = True), - default = sa.func.now(), - nullable = False) - ) - -def make_change_tables(force = False): - engine = g.dbm.engines['change'] - metadata = make_metadata(engine) - table = change_table(metadata) - indices = [ - index_str(table, 'fullname', 'fullname'), - index_str(table, 'date', 'date') - ] - create_table(table, indices, force = force) - return table - -_change_table = make_change_tables() def changed(thing): def _changed(): - d = dict(fullname = thing._fullname, - thing_type = thing._type_id) - try: - _change_table.insert().execute(d) - except sa.exceptions.SQLError: - t = _change_table - t.update(t.c.fullname == thing._fullname, - values = {t.c.date: sa.func.now()}).execute() - from r2.lib.solrsearch import indexed_types - if isinstance(thing, indexed_types): - worker.do(_changed) - - -def _where(cls = None, min_date = None, max_date = None): - t = _change_table - where = [] - if cls: - where.append(t.c.thing_type == cls._type_id) - if min_date: - where.append(t.c.date > min_date) - if max_date: - where.append(t.c.date <= max_date) - if where: - return sa.and_(*where) - -def get_changed(cls = None, min_date = None, limit = None): - t = _change_table - res = sa.select([t.c.fullname, t.c.date], _where(cls, min_date = min_date), - order_by = t.c.date, limit = limit).execute() - return res.fetchall() - -def clear_changes(cls = None, min_date=None, max_date=None): - t = _change_table - t.delete(_where(cls, min_date = min_date, max_date = max_date)).execute() + amqp.add_item('searchchanges_q', thing._fullname, + message_id = thing._fullname) + worker.do(_changed) diff --git a/r2/r2/models/vote.py b/r2/r2/models/vote.py index c9f1666f1..da1634137 100644 --- a/r2/r2/models/vote.py +++ b/r2/r2/models/vote.py @@ -64,10 +64,9 @@ class Vote(MultiRelation('vote', #check for old vote rel = cls.rel(sub, obj) - oldvote = list(rel._query(rel.c._thing1_id == sub._id, - rel.c._thing2_id == obj._id, - data = True)) - + oldvote = rel._fast_query(sub, obj, ['-1', '0', '1']).values() + oldvote = filter(None, oldvote) + amount = 1 if dir is True else 0 if dir is None else -1 is_new = False @@ -90,6 +89,7 @@ class Vote(MultiRelation('vote', oldamount = 0 v = rel(sub, obj, str(amount)) v.author_id = obj.author_id + v.sr_id = sr._id v.ip = ip old_valid_thing = v.valid_thing = (valid_thing(v, karma)) v.valid_user = (v.valid_thing and valid_user(v, sr, karma) diff --git a/r2/r2/public/static/alien-clippy.png b/r2/r2/public/static/alien-clippy.png new file mode 100644 index 0000000000000000000000000000000000000000..bf82a488e592baa5012c10adc16a98ed9a90d74e GIT binary patch literal 2537 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 literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/award.png b/r2/r2/public/static/award.png new file mode 100644 index 0000000000000000000000000000000000000000..312c8a27b82bfe714ad66e562aca083e46e32169 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^+(691!3HGtEm%8%6id3JuOkD)wHsIbBaDE2$r9Iy zlHmNblJdl&R0hYC{G?O`&)mfH)S%SFl*+=BsWw1GIi4<#Ar-fh{`~)Mf4FyL)_+M3 zhTb>PJvs`l3Tz$IHWZ{K{P-xguHof#_O|rv@&*T|6H_V+Po~-c6={3AIEGZ*N?LJG@W7D+2O5O`|7Sl^|DW+={jz$8GYm=p jf1f|XG3js!zd!=R@hqWU5##jBKy3`3u6{1-oD!M<)Bi5> literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/bg-button-remove.png b/r2/r2/public/static/bg-button-remove.png new file mode 100644 index 0000000000000000000000000000000000000000..e5d39657559b0c769c38d880b8635e6e869c650a GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^j6f{H!2~1^=N$M3q$EpRBT9nv(@M${i&7aJQ}UBi z6+Ckj(^G>|6H_V+Po~-c6&ZTEIEGZ*O0tD>B>w literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/cclogo.png b/r2/r2/public/static/cclogo.png new file mode 100644 index 0000000000000000000000000000000000000000..f337b82d633b35413413b80a2b46117da0979654 GIT binary patch literal 5378 zcmV+d75(aoP)e5S__a=)tSEMKKni0J#Xg4%rH2JJO)7^A{e74BAPNP zmKrxvvs;N(*-bQw)Yc|hOJkJ=O=7XSF>Asml0;XN7*`NfBA|f0CMXP<_b>zV>gn$3 z=Y5~M-|dE>r>A+vkhMxr&Ewp2&pH1&|M$Pne=e~si}|E{Y&gJvYWUdGn@=X4Ru3HRh$iCZ zlvf{e`xaxd7E|UAv|QZekc6|$1|Mg$fM$NOePIZ*YLH!XUOjIk*~h%bNi+?^7#|-e z7mm?bu4VB&@Avz23FqQXfBEd5|9+v*;XE5Vawj~W-sSHJF2GB{d8vCXsuTvR7fRgcf_$XR2EC}0+Cow&r&iiYs;Uz`a{tf6=joX z=RFYX=m>vgoE020%R#e=Y4Wm0(wdbt1dit=e|{h0u0c`ioT4U{x@7DpBxS<{7fwB?RN4V=;e?+vRamvoITUi#Gb1?_YQL z?ic;0dajTg8nKtDSX2@bg;{n~W=k5JiS2xGkVW{RBW8nK*$~(u*g}CPEF+f#Q#ZF-c3gb%#W$~B zJ>Iup9oi##N@(rX+uq#znNMF82nODH=N&~+hK7d1;qctKb5&J^&t9-#L3MRCf`dFU z1>s!0*<0?I{izjA9GBaeSzN~9N)Cw-R#R1k0HP?Kn2R7B)dfMo>d_8~L6n>c2#Knx z1wloyCQ7W&V4lk$H9k-JmcUCaViN0&fej#w=A~xkN%t>sFYiQjA0x zj!<-R7+Mh41b&>TI#@tW`P3>-UjqXH(j$*Na@%dUJ@CK-U;gr!UwiE}K=r%d{qF9& z?;aT$*|1^5(@#JBg_~~b>g^xe{YrA*reLvYq@uL<*b6UgiAG}g-1DXHe)qepSFe8J zi6`#6?><0)N=r-2^Uptj|NZyhaKjB#iRVH~E?Y7uBbx=pb7bWB+ke`Q7nfiDshXPU zJKx_7rP1*qUi z;4`Q)RIKDuJX3hZFu=l>L}uW()TIVSYJad6vR*Wd8-Km2cH zWtq?CgC$tIb}bHguniuMr>3UH<#NrKF~jL}PSv+uytB*6f|0*8mn*P!>-Jx)U;pBZ zFLIo8&%Iv*@bA9+t{{lbzimEn;K0n8Gk^S(r=NZH+4}nF_uco;$z*Ed#@CiCDLMVN z3$Vi^Y-7^$QPJ_te@NY<$GZDxufB2g2ahtLgq2Jya!M+zOYttxiWLrRD{+l70bYwm zIk#6CQb(sPD{a1$VZ&zMk0h1T%_QXvD#9a}vO?0_19O_&YW&(@tgzY7-7t*G%mCdJ z(k`uJ4~G*o0`9PCj?0RnaYb%UF~^OB;Nipou{gIht4RIeSaNB3(YQs%qRFNp$8wHL z*a(3xu*Y0NsAnO8Nr#Pufh;X8efi~=fBMs(LSNAmi^b5#WGvl5yqYS>nrzY-Ni4o< z#d9xjdT`B}?|<)mQl>0lz8n}s_Mdv{DQGtAL>{5+<5av0c4NF=&vh%W+wkiR@p$a9 z#~!PztHts0l~-PYaKYK&?K96j4SNC!g{(oca1w~ecGYb~S%y#yo zi_FFugV%h%sAX1Zd{pV~b6;`Q=)0{YH(!%l9~A0K*xJ(LFT64Rx>fCcgUuIBKi>O# z&+t&N^6Rzr7wPhDQ}qUmeF`ztWp5py6sJyHbI+z z_OqYCyP)MOU-`Q*)lkxv9YnK^v>J@BqUX)_2tp=h%wE> zQZvytz;unuRb}Z}mkbn_Pag=Udxw~|-d+bYd)}O3HSWFR^CS@H-`{?PU@`IE27+q4 zI%hDfy1Z4Q$M>hhp=Api4sQCdr8j=Zh((-iX(2a~zbGuj#i(wP4~jttZ{W-=z(_Dg zrHjN>)9VqMi}d{z8iGj|R0nqRM9l5pcqDLm(izkw~N<$ul+Drzp2j*AS0J>WeFnwf891&RPF>Q`f=wgH~ea zy65Wu?HhG7Z(+JSoJCGyEH?heJI==H%A!%{jzi{`R?zCkbaGa6!}Q+aK2@DrTAXIp zLjkE~?yMRQmnLdRH8TGGOe?V85n^o`l3wUdl!+Ek+zsAnje|wlrkg1pxjegs|Szvh~2CMH1zfqM4Qni6`S2i`~%!z*L1 zZ++|ATefWJ?CgS0Z`knbZ+`Rs>C>lsy}sVwzV`O^7hn7zEaJ{P?+k^`TfdQ#Jn9yD zh~e9Eu+!C^jwO5hd;K-lHGcozw(iDTZ&G`@yN`4=EtntLx05d~ty*$v$MJUm)`QH@ z?#*wv%&l4T!Puc09%50)vA#bCOpTAVz1!B}_b)aF4*E?ooWMoQ*?lSyk0{lb@PZRlpR6FA((kS3Uft3dPDcdG$B+ub zrZ~&ciBB*iZqAvER5Emg_0O5dFvmI^2ANHhVxzWlp&6Ff8Gmuj_e9hlw_B8}4aG4i zFR9}Oj~^AohSwcy9V_pM1&cTxuJ|Ou)BZPf)zqnJvL;ESG;Sc3SX|Bz4Y5aMt-+6` z2qD4Ip=SaF~<-7fEnM#Ofdr4 z0i`VW4=tjuzcIE=pDPHLw(xrL4Vs|0&yhgbGqT)7@H&8*X9z- z)(MzM-9?wLlN^ZNClzEo4Nck+EV(q%UChWVk#QaiAhSsrWJ=2u_Q49bEq)`U_d7kW zvYg5BxOpit#ao=U4r=n_cu&N%ExT^fVvujAjU@os!qMsX2O6j*QJ0++nMAd~199jU zYbcDMbDEE3xfb~Ilvd_dY63>sV1kmHWsLd74v*yQiW)8#W2x>cpP5KSYlqsFOzi@3G@7A+dIOghoJ8tsQvfNv(u`?lKlIklXUa`TU3WHLWHL4L>r8d z{;-azAZJkPjyN2jNz7;{4i&Hs3brEXsqB^vEtyCsSIs-8EM6Yfb9w~G8P?$sQn<-* zkMjODb>G(X=wYXlW;sNdfsC0iJlQF{Gw(B?7w-A@Xw8r(PSflcO1{;w;A+A(IjZDO>(=J!gMS0bV0LuH$mnPTC;f3#^eyx4*~8d|6?rxEM7Bj{Mb; z+F4cyvuZI4F|1fP)%(7F_*J9rJth+6ImU|{J>HSoe3kG5d`xBC(|jkPwZ2`BEuvWqckSfYE~HNPoe*y(VRz2cjFW%tbQUf9?lbco2f z+J=-O`^~owR&3Z`{YFOx66+2d4}b@C1Y{>>%G;c*s(#T;W`iaRwm?HdqM$X(BXpjR5H4as1Q|^ahm4*b>BIZAu2vF`Sz?$Y z624KfrSIIzWV|gMSNG>O@I&;1$Dv}@!RF2aQx1NCd*CRpC`8?%6Y?jIZ-=L{Mz^Zf zF^)lUl!;ruKBqU#bHLwg=gQR9p}^Ni=S*{5Qe76ShNocpgr*#ileX~^MioV#LaDPT z!7B?q@wf}$l8wbue?A^{h0`_VMIv-q#V1lHf{g6&?B9tsvlg*~{Y=3L%CybUgjP*d zCc2+@1t{Xj7>}e!`r-rIwT^@AP!}JG*q>@~9PAYsaB^o;C(kY*Z^=3FIs6~b&oez^ zgr6(~Q>XfHEv%{QMRSQT={q`1Srsh#%))t_cXdJuv#uC8W$Z6|RO0)j3alNQKujl4 zNHrYliOslhOU=_E)p@+vv715Ags&Zna0BkZAz6e(Ot2oeT=dpuP1VIk1$R*D_bzX) zktC64MO=-*JK$QRU{5q=%>*vwaS?B-I6B!q(TG3P4)!L(*}@ax=nN6TRp!*!ni@}s zk28basblR*s9ha8Y9&IJ8TV=ePo;#*rXgua=UK=B;Y*raPnSCzu?(5_WR`TQ**@T8 zpD0yu+(bNUhaVTTKxhez?}T!+Axlq_HKXI`7|y-tzXV$nrmT8WBQ^?UpH){WoaVqe zF&sSEcYHXGM0tT6f!)hS)ux76&357N4>0R^#)UA&q1`f?%e*s6%k8eGDM*{|+O-SU z%2VHK>2Wh25|b&`Gzc!^SRUV9fB?Rm1|%O$WD@{VjKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cxg@$ctjR6FbI`^Fyp;6`3j&Qdx@v7EBj3@5pi?FE%UUl0)-??Tq8=H^K)}k z^GX<;i&7IyQd1PlGfOfQ+&z5*!W;R-fr@r`x;TbNT=qSqSav8tp!H$DjExdsWbbUN zYXZ|g?qZiYo$fO#lD@ literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/css/reddit-ie6-hax.css b/r2/r2/public/static/css/reddit-ie6-hax.css index daaff143f..77140ccce 100644 --- a/r2/r2/public/static/css/reddit-ie6-hax.css +++ b/r2/r2/public/static/css/reddit-ie6-hax.css @@ -144,3 +144,23 @@ div.popup { .shirt .shirt-container .main.red .caption.big .byline { background-image: non; } + +.usertext .bottom-area .usertext-buttons {display: inline; } + +.arrow.upmod { + background-image: url(/static/aupmod.gif); + background-position: 0 0; +} +.arrow.downmod { + background-image: url(/static/adownmod.gif); + background-position: 0 0; +} +.arrow.up { + background-image: url(/static/aupgray.gif); + background-position: 0 0; +} +.arrow.down { + background-image: url(/static/adowngray.gif); + background-position: 0 0; +} + diff --git a/r2/r2/public/static/css/reddit-ie7-hax.css b/r2/r2/public/static/css/reddit-ie7-hax.css index 7a5cb7cc9..676c7b0c8 100644 --- a/r2/r2/public/static/css/reddit-ie7-hax.css +++ b/r2/r2/public/static/css/reddit-ie7-hax.css @@ -2,3 +2,11 @@ border: none; height: 17px; } + +.entry.unvoted .score.unvoted { display: inline; } +.entry.unvoted div.score.unvoted { display: inline; } + +/* Award name font is too big in IE */ +.award-square .award-name { + font-size:18px; +} diff --git a/r2/r2/public/static/css/reddit.css b/r2/r2/public/static/css/reddit.css index 8424de02f..f61fb5383 100644 --- a/r2/r2/public/static/css/reddit.css +++ b/r2/r2/public/static/css/reddit.css @@ -55,7 +55,7 @@ a:focus { -moz-outline-style: none; } -webkit-border-radius: 7px; } -.rounded .morelink { +.rounded .morelink { -webkit-border-top-right-radius: 6px; -moz-border-radius-topright: 6px; } @@ -94,7 +94,7 @@ input[type=checkbox], input[type=radio] { margin-top: .4em; } .selected { font-weight: bold; } .flat-list {list-style-type: none; display: inline;} -.flat-list li {display: inline;} +.flat-list li {display: inline; } .flat-list form {display: inline; } .flat-list .selected a { color: orangered; } @@ -102,7 +102,7 @@ ul.flat-vert {text-align: left;} .flat-vert .separator { margin: 0 } .flat-vert.title { - font-family:helvetica,arial,verdana,sans-serif; + font-family:arial,verdana,helvetica,sans-serif; color: #777; font-size: 18px; font-weight: normal; @@ -130,11 +130,11 @@ ul.flat-vert {text-align: left;} right: 5px; } -#header-bottom-left { +#header-bottom-left { font-size: larger; } -#header-bottom-right { +#header-bottom-right { position: absolute; right: 0px; bottom: 0px; @@ -163,7 +163,7 @@ ul.flat-vert {text-align: left;} background-color: #ff9; } -.dropdown { +.dropdown { cursor: default; display: inline; position: relative; @@ -195,7 +195,8 @@ ul.flat-vert {text-align: left;} } .dropdown.heavydrop .selected{ - background: white url(/static/droparrow.gif) no-repeat scroll center right; + background: white none no-repeat scroll center right; + background-image: url(/static/droparrow.gif); border: 1px solid gray; padding: 2px; padding-right: 23px; @@ -205,20 +206,22 @@ ul.flat-vert {text-align: left;} .dropdown.lightdrop .selected { position: relative; - background: transparent url(/static/droparrowgray.gif) no-repeat scroll center right; + background: transparent none no-repeat scroll center right; + background-image: url(/static/droparrowgray.gif); padding-right: 21px; text-decoration: underline; color: gray; } -.drop-choices.lightdrop { +.drop-choices.lightdrop { margin-top: 2px; } /*tab drop*/ .dropdown.tabdrop .selected { position: relative; - background: white url(/static/droparrowgray.gif) no-repeat scroll center right; + background: white none no-repeat scroll center right; + background-image: url(/static/droparrowgray.gif); padding: 2px 21px 1px 5px; margin-left: 3px; border: 1px solid #5f99cf; @@ -247,12 +250,12 @@ ul.flat-vert {text-align: left;} margin: 0px 3px; } -.tabmenu li a { +.tabmenu li a { padding: 2px 6px 0 6px; background-color: #eff7ff; } -.tabmenu li.selected a{ +.tabmenu li.selected a{ color: orangered; background-color: white; border: 1px solid #5f99cf; @@ -279,116 +282,100 @@ ul.flat-vert {text-align: left;} /* side box menus */ -.side { +.side { float: right; background-color: white; margin: 0px 5px 0 5px; width: 300px; } -.side .spacer { - margin: 7px 0; +.side .spacer { + margin: 7px 0 12px 0; } .morelink { - background-color:#FFFFFF; - color:#369; display:block; + text-align: center; + position: relative; + + -moz-border-radius-topleft: 6px; + -moz-border-radius-bottomleft: 6px; + + border: 1px solid #c4dbf1; + + background: white none repeat-x scroll center left; + background-image: url(/static/gradient-button.png); + font-size:150%; font-weight:bold; + letter-spacing:-1px; - padding:5px 10px; - text-transform:uppercase; - border: 1px solid #fff; + line-height: 29px; + height: 29px; } -.morelink.blah:hover { - background-color: #fff; - color: #369; +.morelink:hover, .mlh { + border-color: #879eb4; + background-image: url(/static/gradient-button-hover.png); } -.morelink.blah { - background-color: #369; - color: #fff; - border: 1px solid #fff; -} - -.morelink:hover { - color: #fff; - background-color: #369; - border: 1px solid #369; +.morelink a { + display: block; + width: 100%; + color:#369; } -.sidebox { +.morelink:hover a { + color:white; +} + +.morelink .nub { + position: absolute; + top: -1px; + right: -1px; + height: 31px; + width: 24px; + background: white none no-repeat scroll center left; + background-image: url(/static/gradient-nub.png); /* SPRITE */ +} + +.morelink:hover .nub, .mlhn { + background-image: url(/static/gradient-nub-hover.png); /* SPRITE */ +} + +/* raised box */ + +.raisedbox { + padding: 5px; + background: #E0E0E0; border: 1px solid gray; - padding-left: 44px; } -.sidebox .spacer { margin: 0 0 5px 0 } -.sidebox.create { - background: url(/static/create-a-reddit.png) no-repeat scroll center left; +.raisedbox h4 { margin-bottom: 3px } +.raisedbox li {margin-bottom: 2px;} + +.sidebox .spacer { + margin-top: 10px; + padding: 5px 0 0 44px; + min-height: 41px; + background: white none no-repeat scroll top left; } -.sidebox.submit {background: url(/static/submit-alien.png) no-repeat scroll center left;} -.sidebox .morelink { text-transform: none; } + +.sidebox.create .spacer { + background-image: url(/static/create-a-reddit.png); /* SPRITE */ +} + +.sidebox.submit .spacer { + background-image: url(/static/submit-alien.png); /* SPRITE */ +} + .sidebox .subtitle { margin-left: 10px; color: dimgray; font-size: 110%; } -/* raised box */ -.raisedbox { - padding: 5px; - background: #E0E0E0; - border: 1px solid gray; -} - -.raisedbox h3 { - font-size: 160%; - margin-bottom: 0px; - color: #333; -} -.raisedbox h4 { margin-bottom: 3px } - -.raisedbox li {margin-bottom: 2px;} - -.subreddit-info {padding-bottom: 3px } -.subreddit-info .moderate { color: orangered; } - -.subreddit-info .subscribe-button { - margin-right: 5px; - font-size: larger; -} - -.subreddit-info .label {color: #404040;} -.subreddit-info .state-button { display:block } -.subreddit-info .spacer { margin: 10px 0px 0px 0px } - -.raisedbox .flat-vert { } -.raisedbox .flat-vert .separator { display: none; } -.raisedbox .flat-vert a {font-size: larger;} - -/* feature disabled. need to go through this to see what's relevant -before enabling */ -/* -.raisedbox .edit { - color: gray; - text-decoration: none; - font-size: x-small; -} - -.raisedbox #avatar { float: left; - text-align: center; } -.raisedbox #avatar img { - width: 64px; - height: 64px; - display: block; - border: 1px solid black; - margin-right: 10px; -} -.raisedbox #avatar a { padding: 0px; } -*/ .infotable { margin-top: 5px; margin-bottom: 10px; } .infotable .small { font-size: smaller; } @@ -407,7 +394,7 @@ before enabling */ .profile-attr .value {color: #404040; margin-right: 5px; } -.profile-attr .md { +.profile-attr .md { margin-left: 10px; margin-top: 5px; border-color: #B2B2B2 #D0D0D0 #D0D0D0 #B2B2B2; @@ -415,7 +402,7 @@ before enabling */ border-width: 1px; padding: 10px; } -.profile-attr .md ul { +.profile-attr .md ul { float: none; list-style-type: disc; margin-left: 15px; @@ -429,7 +416,7 @@ before enabling */ /* thing rendering */ -.preload { +.preload { position: absolute; top: -1000px; left: -1000px; @@ -443,14 +430,25 @@ before enabling */ cursor: pointer; background-position: center center; background-repeat: no-repeat; + width: 15px; + margin-left: auto; + margin-right: auto; } -.arrow.upmod { background-image: url(/static/aupmod.gif); } -.arrow.downmod { background-image: url(/static/adownmod.gif); } -.arrow.up { background-image: url(/static/aupgray.gif); } -.arrow.down { background-image: url(/static/adowngray.gif); } +.arrow.upmod { + background-image: url(/static/aupmod.gif); /* SPRITE */ +} +.arrow.downmod { + background-image: url(/static/adownmod.gif); /* SPRITE */ +} +.arrow.up { + background-image: url(/static/aupgray.gif); /* SPRITE */ +} +.arrow.down { + background-image: url(/static/adowngray.gif); /* SPRITE */ +} -.midcol { +.midcol { float: left; margin-right: 4px; margin-left: 7px; @@ -473,13 +471,14 @@ before enabling */ .tagline .friend { color: orangered } .tagline .submitter { color: #0055df } .tagline .moderator { color: #228822 } -.tagline .admin { color: #ff0000; } +.tagline .admin { color: #ff0011; } .tagline a.author.admin { font-weight: bold } .tagline a:hover { text-decoration: underline } .media-button .option { color: red; } -.media-button .option.active { - background: transparent url(/static/reddit-button-play.gif) no-repeat scroll right center; +.media-button .option.active { + background: transparent none no-repeat scroll right center; + background-image: url(/static/reddit-button-play.gif); /* SPRITE */ padding-right: 15px; color: #336699; } @@ -489,7 +488,7 @@ before enabling */ .thing .title { color: blue; padding: 0px; overflow: hidden; } .thing .title:visited { color: #551a8b } .thing .title.click { color: #551a8b } - + .thing .title.loggedin { color: blue } .thing .title.loggedin:visited { color: #551a8b } .thing .title.loggedin.click { color: #551a8b } @@ -500,12 +499,12 @@ before enabling */ .nextprev { color: gray; font-size: larger; margin-top: 10px;} /* corner help */ -.help a { +.help a { color: #808080; text-decoration: underline; } -.help a.open { +.help a.open { margin: 0px 5px 5px 0; position: absolute; right: 0px; @@ -513,7 +512,7 @@ before enabling */ } -.help.help-cover { +.help.help-cover { background-color: #F8F8F8; border: 1px solid gray; font-size: 110%; @@ -530,7 +529,7 @@ before enabling */ } /* organic listing */ -.organic-listing { +.organic-listing { border: solid 1px gray; padding: 0; overflow: hidden; @@ -538,13 +537,16 @@ before enabling */ min-height: 50px; } -.organic-listing .link { +.organic-listing .link { background-color: #F8F8F8; +} +.organic-listing .link, +.organic-listing .link.promotedlink { padding: 5px 7em 10px 0; margin-bottom: 0px; } -.organic-listing .nextprev { +.organic-listing .nextprev { margin: 0px; position: absolute; right: 0px; @@ -556,14 +558,22 @@ before enabling */ .organic-listing .nextprev img:hover { cursor: pointer; border: solid 1px #336699; } .organic-listing .nextprev img:active { margin: 6px 4px 1px 1px;} -.promoted { - background-color: #EFF7FF; +.link.promotedlink { + /*background-color: lightgreen; */ border: 1px solid gray; - padding: 5px 0 5px 0; + padding: 5px 0 5px 3px; overflow: hidden; position: relative; } - +.link.promotedlink.unpaid { background-color: #FFC; } +.link.promotedlink.unseen { background-color: #FFC; } +.link.promotedlink.accepted { background-color: #9F9; } +.link.promotedlink.rejected { background-color: #FF9A9A; } +.link.promotedlink.accepted { background-color: #9F9; } +.link.promotedlink.pending { background-color: #BFC; } +.link.promotedlink.promoted { background-color: #EFF7FF; } +.link.promotedlink.finished { background-color: #DDD; } +#promo-form + form #img-preview-container { display: none; } .promoted-list { font-size: larger; } .promoted-list .unpromote-button { display: inline } @@ -580,7 +590,7 @@ before enabling */ right: 6.4em; } -.sponsored-tagline { +.sponsored-tagline { color: #808080; bottom: 0; margin: 0 5px 5px 0; @@ -600,7 +610,8 @@ before enabling */ .menuarea { border-bottom: 1px dotted gray; padding: 5px 10px; - margin: 5px 310px 5px 5px; + margin: 5px; + overflow: hidden; font-size: larger; } @@ -619,7 +630,7 @@ before enabling */ color: gray; } -.commentarea > .usertext { +.commentarea > .usertext { margin: 0 0 10px 10px; overflow: auto; } @@ -637,6 +648,18 @@ before enabling */ vertical-align: middle; } +.infobar.red { + padding: 5px; + background-color: #FFAEAE; + border-color: red; +} + +.infobar.red img { + float: left; + margin-right: 5px; +} + + /* markdown */ .md { max-width: 60em; overflow: auto; font-size: small; } .md p, .md h1 { margin: 5px 0} @@ -664,15 +687,17 @@ a.star { text-decoration: none; color: #ff8b60 } .even { } /* buttons on main link style */ -.entry .buttons li { +.entry .buttons li { display: inline; - padding: 0 4px; border: none; + padding-right: 4px; +} +.entry .buttons li + li { + padding-left: 4px; } -.entry .buttons li.first {padding-left: 0px;} -.entry .buttons li a { +.entry .buttons li a { color: #888; font-weight: bold; padding: 0 1px; @@ -698,7 +723,7 @@ a.star { text-decoration: none; color: #ff8b60 } .link .score {text-align: center; color: #c6c6c6;} .link .title {font-size:medium; font-weight: normal; margin-bottom: 1px;} -.link .child h3 { +.link .child h3 { margin: 15px; text-transform: none; font-size: medium; @@ -710,7 +735,7 @@ a.star { text-decoration: none; color: #ff8b60 } .link .score.likes { color: #FF8B60; } .link .score.dislikes { color: #9494FF; } -.link .rank { +.link .rank { float:left; margin-top: 15px; color: #c6c6c6; @@ -744,14 +769,6 @@ a.star { text-decoration: none; color: #ff8b60 } /* widget styling */ .gadget { font-size: x-small; - border: 1px solid gray; - padding: 5px; -} - -.gadget h2 { - padding-left: 49px; - font-size: 150%; - margin-bottom: 5px; } .gadget .midcol { @@ -786,12 +803,12 @@ a.star { text-decoration: none; color: #ff8b60 } padding-left: 17px; padding-bottom: 10px; } .comment .collapsed a { color: gray; } -.comment .expand { +.comment .expand { font-style: normal; margin-left: 5px; margin-right: 5px; padding: 1px; } -.comment .expand:hover { +.comment .expand:hover { text-decoration: none; color: white; background-color: #369; @@ -807,9 +824,8 @@ textarea.gray { color: gray; } .deepthread { padding-right: 30px; - background-image: url(/static/continue-thread.png); - background-repeat: no-repeat; - background-position: center right; + background: transparent none no-repeat scroll center right; + background-image: url(/static/continue-thread.png); } .deepthread a { font-size: larger; color: #336699 } .deepthread a:hover { text-decoration: underline} @@ -819,33 +835,90 @@ textarea.gray { color: gray; } .morecomments a:hover { text-decoration: underline} .morecomments .gray {font-weight: normal; color: gray} -.message {margin: 10px} +.message {margin: 10px; margin-bottom: 20px;} +.message .buttons, +.message .md { margin-left: 15px; } +.message .entry .parent { + border: 1px solid #336699; + max-width: 60em; + margin: 3px 10px; +} +.message .subject .title { + font-weight: normal; + font-style: italic; + margin-left: 10px; +} +.message .parent-link { + margin-left: 12px; + padding: 0 2px; + font-weight: bold; +} +.message .child { margin-left: 15px; } + .message .head.new {color:orangered } .message .subject { font-weight: bold; font-size: larger; } +.clippy img { + float: left; +} + +.clippy-bubble { + background-color:#fffdd7; + border: solid black 1px; + width: 350px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + margin-left: 5px; + margin-bottom: 15px; + padding: 7px; + float: left; +} + +.clippy-headline { + font-weight:bold; + margin-bottom: 0.5em; +} + +.clippy-bubble ul { + list-style-type: disc; + list-style-image: url(/static/clippy-bullet.png); + padding-left: 15px; +} + +.clippy-bubble li { + margin-top: 0.5em; +} + .subreddit { margin-bottom: 10px; } .subreddit p { margin-top: 0px; margin-bottom: 1px; } .subreddit .description {font-size: small; max-width: 60em;} .subreddit .key {display: block;} .subreddit .title { font-size: medium; margin-right: 5px; } -.subreddit .midcol { margin-right: 5px } +.subreddit .midcol { margin-right: 5px; margin-top: 5px; text-align: right; } -.sr-toggle-button { +.fancy-toggle-button { display: block; margin-bottom: 5px; - cursor: pointer; } -.sr-toggle-button a.option.active { - display: block; - width: 54px; - height: 18px; +.fancy-toggle-button .active { + border: 1px solid #444; + padding: 1px 6px; + background: white none repeat-x scroll center left; + + color: white; + font-size: 10px; + font-weight: bold; + + line-height: 20px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; } -.sr-toggle-button .remove { background-image: url(/static/sr-remove-button.png)} -.sr-toggle-button .add { background-image: url(/static/sr-add-button.png) } - -.sr-type-img { - margin-right: 2px; +.fancy-toggle-button .remove { + background-image: url(/static/bg-button-remove.png); +} +.fancy-toggle-button .add { + background-image: url(/static/bg-button-add.png); } .commentbody.border { background-color: #ffc; padding-left: 5px} @@ -870,7 +943,7 @@ textarea.gray { color: gray; } .share-summary { width: 95%; margin-top: 10px; } .share-summary .head td { width: 50%; font-size: large; text-align: center } .share-summary td { vertical-align: top;} -.share-summary > tbody > tr > td { +.share-summary > tbody > tr > td { padding-left: 10px; padding-bottom: 10px; } @@ -886,12 +959,6 @@ textarea.gray { color: gray; } border-right: #E0E0E0 solid 1px; } -.sponsored .asterisk { - width: 15px; - background: url(/static/asterisk.png); - background-repeat: no-repeat } - - /* footer */ .footer-parent { padding-top: 40px; @@ -927,7 +994,7 @@ textarea.gray { color: gray; } } .server-status { width: 300px; } -.server-status table { +.server-status table { font-size: xx-small; margin-left: 5px; border-top: #BCBCBC solid 1px; @@ -935,6 +1002,7 @@ textarea.gray { color: gray; } border-bottom: #E0E0E0 solid 1px; border-right: #E0E0E0 solid 1px; margin-bottom: 5px; + width: 290px; } .server-status td { padding-right: 2px; padding-left: 2px; } .server-status .bar { height: 5px; background-color: blue; } @@ -948,7 +1016,7 @@ textarea.gray { color: gray; } .server-status .load7 { background-color: #ffdb81; } .server-status .load8 { background-color: #FF9191; } .server-status .load9 { background-color: #FF0000; color: #FFFFFF } -.server-status tr.down > * { +.server-status tr.down > * { background-color: #C0C0C0; text-decoration: line-through; } @@ -963,29 +1031,29 @@ textarea.gray { color: gray; } .server-status tr.title-region.empty:hover > th { text-decoration: none; } -.server-status .pegged { +.server-status .pegged { background-color: red; font-weight: bold; color: #FFFFFF; } -.server-status .membar { +.server-status .membar { height:11px; border:1px solid white; background-color:#6699FF; position: relative; } -.server-status .membar span { +.server-status .membar span { position: absolute; font-size: smaller; } -.server-status .cpu50 { +.server-status .cpu50 { height: 5px; background-color:green; border:1px solid white; border-bottom: none; } -.server-status .cpu300 { +.server-status .cpu300 { height: 5px; background-color:red; border:1px solid white; @@ -997,11 +1065,11 @@ textarea.gray { color: gray; } .orangered { color: orangered; } .logout { display: inline; } -.login-form-side { +.login-form-side { border: 1px solid gray; } -.login-form-side input { +.login-form-side input { border: 1px solid gray; width: 138px; height: 17px; @@ -1010,11 +1078,11 @@ textarea.gray { color: gray; } padding: 1px; } -.login-form-side .error { +.login-form-side .error { margin: 5px; } -#remember-me { +#remember-me { margin: 5px; } @@ -1075,23 +1143,18 @@ textarea.gray { color: gray; } margin-left: 10px; } -/*.searchpane { margin-left: 20px } -.searchpane h2 { margin-bottom: 3px } -.searchpane p { margin-bottom: 5px; margin-top:3px; } -.searchpane a { color: #369 }*/ - -.searchpane { - margin: 5px 310px 5px 0px; +.searchpane { + margin: 5px 305px 5px 0px; } .searchpane #search input[type=text] { } -.searchpane .summary { +.searchpane .summary { font-weight: bold; float: right; } -.searchpane .clearleft { +.searchpane .clearleft { margin-bottom: 10px; } @@ -1104,14 +1167,14 @@ textarea.gray { color: gray; } .divide { border-right: 2px solid #D3D3D3; } -.loginform { +.loginform { float: left; width: 45%; padding-left: 15px; padding-right: 15px; } -.loginform h3 { +.loginform h3 { margin-bottom: 0; margin-top: 10px; font-size: large; @@ -1119,7 +1182,7 @@ textarea.gray { color: gray; } font-variant: small-caps; color: #404040; } -.loginform p { +.loginform p { text-align: left; margin-bottom: 10px; color: #606060; @@ -1138,7 +1201,7 @@ textarea.gray { color: gray; } .loginform input.logtxt { width: 125px; } .loginform input[type=text], -.loginform input[type=password] { +.loginform input[type=password] { width: 125px; border: 1px solid #A0A0A0; margin-top: 2px; @@ -1146,7 +1209,7 @@ textarea.gray { color: gray; } padding: 1px; } -.loginform #captcha { +.loginform #captcha { width: 250px; } @@ -1180,14 +1243,14 @@ textarea.gray { color: gray; } border-width: 1px; } -.popup h1 { +.popup h1 { text-align: center; font-size: large; font-weight: normal; color: orangered; } -.popup h2 { +.popup h2 { text-align: center; font-size: small; margin-top: 0px; @@ -1211,18 +1274,18 @@ textarea.gray { color: gray; } .oldbylink a { background-color: #F0F0F0; margin: 2px; color: gray} -.details { +.details { font-size: x-small; margin-bottom: 10px; } .details span { margin: 0 5px 0 5px; } -.details th { +.details th { text-align: right; padding-right: 5px; font-weight: bold; } -.details td { +.details td { vertical-align: top; } @@ -1262,14 +1325,14 @@ textarea.gray { color: gray; } /* Buttons specific */ -.button { +.button { border-collapse: collapse; color: gray; text-align: center; margin: 1px; } -.button #cover { +.button #cover { position: relative; } @@ -1277,7 +1340,7 @@ textarea.gray { color: gray; } background: white; } -.button #popup { +.button #popup { position: absolute; width: 80%; z-index: 1001; @@ -1315,19 +1378,19 @@ textarea.gray { color: gray; } width: 18px; float: left; } -.button .blog1 .score { +.button .blog1 .score { float: center; margin-top: 2px; margin-right: 5px; } .button .blog2 { font-size: small; } -.button .blog2 .arrow { width: 100% } +.button .blog2 .arrow { width: 15px; margin-left: auto; margin-right: auto; } .button .blog2 .bottomreddit { color: black; background-color: #c7def7; font-size: small; } .button .blog3 { font-size: small; border: none; } .button .blog3 .left { float: left; width: 50%; } -.button .blog3 .arrow { width: 100% } +.button .blog2 .arrow { width: 15px; margin-left: auto; margin-right: auto; } .button .blog3 .right { float: right; margin-top: 5px; } @@ -1347,13 +1410,13 @@ textarea.gray { color: gray; } border-width: 1px; } -.blog5 .votes { +.blog5 .votes { height: 25px; background-color: #F8F8F1; border: 1px solid #CCC; padding-top: 5px; } -.blog5 .arrow { +.blog5 .arrow { margin-right: 15px; margin-left: 5px; color: black; @@ -1389,7 +1452,7 @@ textarea.gray { color: gray; } .instructions .buttons li { margin-top: 1em; border-bottom: 1px solid #e0e0e0; padding-bottom: 1em;} -.instructions code { +.instructions code { display: block; font-family: monospace; font-size: small; @@ -1532,10 +1595,18 @@ textarea.gray { color: gray; } padding-left: 16px; } -.toolbar .arrow.upmod { background-image: url(/static/aminiupmod.gif); } -.toolbar .arrow.downmod { background-image: url(/static/aminidownmod.gif); } -.toolbar .arrow.up { background-image: url(/static/aminiupgray.gif); } -.toolbar .arrow.down { background-image: url(/static/aminidowngray.gif); } +.toolbar .arrow.upmod { + background-image: url(/static/aminiupmod.gif); +} +.toolbar .arrow.downmod { + background-image: url(/static/aminidownmod.gif); +} +.toolbar .arrow.up { + background-image: url(/static/aminiupgray.gif); +} +.toolbar .arrow.down { + background-image: url(/static/aminidowngray.gif); +} .toolbar-status-bar { border-top: solid #336699 1px; @@ -1611,7 +1682,7 @@ form input[type=checkbox], form input[type=radio] {margin: 2px .5em 0 0; } -.pretty-form { +.pretty-form { font-size: larger; vertical-align: top; } @@ -1628,7 +1699,7 @@ form input[type=radio] {margin: 2px .5em 0 0; } padding: 2px; } -.pretty-form .infobar { +.pretty-form .infobar { width: 285px; margin: 5px; } @@ -1643,11 +1714,15 @@ form input[type=radio] {margin: 2px .5em 0 0; } .pretty-form th { text-align: right } /* delete page */ -.delete-field { +.delete-field { background-color: white; padding: 10px; } +.delete-field td { + vertical-align: top; +} + /*pref page boxes*/ .pretty-form.short-text input[type=text], .pretty-form.short-text textarea, @@ -1663,7 +1738,7 @@ form input[type=radio] {margin: 2px .5em 0 0; } .opt-form form { display: inline; } /* pref table - used for preferences and edit subreddit pages */ -.preftable th { +.preftable th { padding: 10px; font-weight: bold; vertical-align: top; @@ -1700,7 +1775,7 @@ form input[type=radio] {margin: 2px .5em 0 0; } color: #369; font-weight: bold;} .stats td.ri { padding-left: 20px; text-align: right} -.thumbnail { +.thumbnail { float: left; margin: 0px 5px; overflow: hidden; @@ -1717,11 +1792,11 @@ form input[type=radio] {margin: 2px .5em 0 0; } .image-upload span { padding-left: 5px; } -ul#image-preview-list { +ul#image-preview-list { margin: 20px 320px 20px 20px; font-size:larger; } -ul#image-preview-list li { +ul#image-preview-list li { padding-bottom: 10px; margin-bottom: 20px; vertical-align: top; @@ -1731,7 +1806,7 @@ ul#image-preview-list li { position: relative; } -ul#image-preview-list .preview { +ul#image-preview-list .preview { width: 100px; float: left; display: block; @@ -1739,15 +1814,15 @@ ul#image-preview-list .preview { max-height: 100px; overflow: hidden; } -ul#image-preview-list .preview img { +ul#image-preview-list .preview img { max-width: 100px; padding: auto; } -ul#image-preview-list .description { +ul#image-preview-list .description { vertical-align: top; margin-left: 105px; } -ul#image-preview-list .description pre { +ul#image-preview-list .description pre { display: inline; padding: 5px; } @@ -1762,7 +1837,7 @@ ul#image-preview-list .description pre { .sheets .btn.right { float: right; margin-right: 3px;} -#validation-errors { +#validation-errors { margin-left: 40px; margin-top: 10px; list-style-type: disc; @@ -1775,7 +1850,7 @@ ul#image-preview-list .description pre { #validation-errors a:hover { text-decoration: underline; } #validation-errors pre { padding: 10px; color: black; } -#preview-table { +#preview-table { padding-right: 15px; } #preview-table > table { @@ -1789,7 +1864,7 @@ ul#image-preview-list .description pre { #preview-table > table > tbody > tr { padding-bottom: 10px; } #preview-table > table > tbody > tr > td { padding: 5px; padding-right: 15px;} -#preview-table > table > tbody > tr > th { +#preview-table > table > tbody > tr > th { padding: 5px; padding-right: 15px; font-weight: bold; vertical-align: top; @@ -1842,7 +1917,7 @@ ul#image-preview-list .description pre { clear: left; } -.socialite.instructions .features { +.socialite.instructions .features { padding-left: 15px; max-width: 60em; } @@ -1856,7 +1931,8 @@ ul#image-preview-list .description pre { } .socialite a.installbutton { - background: transparent url(/static/socialite/installbutton-end.png) no-repeat scroll top right; + background: transparent none no-repeat scroll top right; + background-image: url(/static/socialite/installbutton-end.png); /* SPRITE */ color: #FFF; display: block; float: left; @@ -1868,7 +1944,8 @@ ul#image-preview-list .description pre { } .socialite a.installbutton span { - background: transparent url(/static/socialite/installbutton.png) no-repeat; + background: transparent none no-repeat; + background-image: url(/static/socialite/installbutton.png); /* SPRITE */ display: block; line-height: 30px; padding: 10px 0 10px 17px; @@ -1882,7 +1959,7 @@ ul#image-preview-list .description pre { background-position: bottom left; } -#sr-header-area { +#sr-header-area { padding: 3px 0px 3px 5px; background-color: #f0f0f0; white-space: nowrap; @@ -1896,8 +1973,9 @@ ul#image-preview-list .description pre { color: orangered; } -.dropdown.srdrop .selected { - background: transparent url(/static/droparrowgray.gif) no-repeat scroll center right; +.dropdown.srdrop .selected { + background: transparent none no-repeat scroll center right; + background-image: url(/static/droparrowgray.gif); display: inline-block; vertical-align: bottom; padding-right: 21px; @@ -1938,22 +2016,17 @@ ul#image-preview-list .description pre { #sr-more-link:hover {text-decoration: underline;} -.subscription-box { - border: 1px solid gray; - padding: 0 10px; - } - -.subscription-box li { +.subscription-box li { clear: left; margin-bottom: 10px; } -.subscription-box .sr-toggle-button { +.subscription-box .fancy-toggle-button { margin-right: 5px; float: left; } -.subscription-box .title { +.subscription-box .title { font-size: medium; color: blue; margin-right: 5px; @@ -1967,7 +2040,7 @@ ul#image-preview-list .description pre { #sr { margin-left: 0px } -#sr-list-wrapper { +#sr-list-wrapper { width: 454px; height: 200px; border: 1px solid gray; @@ -1977,9 +2050,10 @@ ul#image-preview-list .description pre { position: relative; } -#sr-list-cover { +#sr-list-cover { position: absolute; - background: gray url(/static/throbber.gif) no-repeat scroll center center; + background: gray none no-repeat scroll center center; + background-color: url(/static/throbber.gif); height: 100%; width: 100%; opacity: .7; @@ -1988,7 +2062,7 @@ ul#image-preview-list .description pre { display: none; } -#sr-list { +#sr-list { overflow: auto; position: absolute; height: 100%; @@ -2004,29 +2078,30 @@ ul#image-preview-list .description pre { padding: 3px 3px 3px 0; } -.sr-description { +.sr-description { padding: 3px } -.sr-row { +.sr-row { cursor: default; } -.sr-row.sr-selected { - background: #EFF7FF url(/static/rightarrow.png) no-repeat scroll 0px 5px; +.sr-row.sr-selected { + background: #EFF7FF none no-repeat scroll 0px 5px; + background-image: url(/static/rightarrow.png); /* SPRITE */ } -.sr-arrow { +.sr-arrow { width: 10px; height: 12px; } -#sr-autocomplete-area { +#sr-autocomplete-area { position: relative; z-index: 100; } -#sr-drop-down { +#sr-drop-down { position: absolute; width: 498px; border: 1px solid gray; @@ -2035,15 +2110,15 @@ ul#image-preview-list .description pre { left: 0; } -#sr-drop-down table { +#sr-drop-down table { width: 100%; } -.sr-name-row { +.sr-name-row { cursor: default; } -.sr-name-row.sr-selected { +.sr-name-row.sr-selected { background-color: #369; color: white; } @@ -2058,7 +2133,7 @@ ul#image-preview-list .description pre { font-size: small; } -#suggested-reddits ul { +#suggested-reddits ul { } @@ -2070,28 +2145,28 @@ ul#image-preview-list .description pre { /*** new menu shit ***/ -.formtabs-content { +.formtabs-content { width: 520px; border-top: 4px solid #5f99cf; padding-top: 10px; } -.formtabs-content .infobar { +.formtabs-content .infobar { margin: 0; padding: 5px; } -ul.tabmenu.formtab { +ul.tabmenu.formtab { display: block; padding-left: 10px; font-size: larger; } -.tabmenu.formtab li { +.tabmenu.formtab li { margin: 0; } -.tabmenu.formtab a { +.tabmenu.formtab a { font-weight: normal; outline: none; padding: 0px 12px; @@ -2101,7 +2176,7 @@ ul.tabmenu.formtab { border-bottom: none; } -.tabmenu.formtab .selected a { +.tabmenu.formtab .selected a { color:white; font-size: 130%; background-color: #5f99cf; @@ -2115,12 +2190,12 @@ ul.tabmenu.formtab { margin: 5px 0 5px 0; } -.expando-content { +.expando-content { display: none; } -.expando-button { +.expando-button { float: left; height: 23px; width: 23px; @@ -2128,22 +2203,45 @@ ul.tabmenu.formtab { background: white none no-repeat scroll center center; } -.expando-button.selftext.collapsed {background-image: url(/static/blog-collapsed.png);} -.expando-button.selftext.collapsed:hover, .eb-sch {background-image: url(/static/blog-collapsed-hover.png);} +.expando-button.selftext.collapsed { + background-image: url(/static/blog-collapsed.png); /* SPRITE */ +} +.expando-button.selftext.collapsed:hover, .eb-sch { + background-image: url(/static/blog-collapsed-hover.png); /* SPRITE */ +} .expando-button.selftext.expanded, .eb-se { margin-bottom: 5px; - background-image: url(/static/blog-expanded.png); + background-image: url(/static/blog-expanded.png); /* SPRITE */ +} +.expando-button.selftext.expanded:hover, .eb-seh { + background-image: url(/static/blog-expanded-hover.png); /* SPRITE */ } -.expando-button.selftext.expanded:hover, .eb-seh {background-image: url(/static/blog-expanded-hover.png);} -.expando-button.video.collapsed {background-image: url(/static/vid-collapsed.png);} +.expando-button.video.collapsed { + background-image: url(/static/vid-collapsed.png); /* SPRITE */ +} -.expando-button.video.collapsed:hover, .eb-vch {background-image: url(/static/vid-collapsed-hover.png);} -.expando-button.video.expanded, .eb-ve {background-image: url(/static/vid-expanded.png);} -.expando-button.video.expanded:hover, .eb-veh {background-image: url(/static/vid-expanded-hover.png);} +.expando-button.video.collapsed:hover, .eb-vch { + background-image: url(/static/vid-collapsed-hover.png); /* SPRITE */ +} + +.expando-button.video.expanded, .eb-ve { + background-image: url(/static/vid-expanded.png); /* SPRITE */ +} +.expando-button.video.expanded:hover, .eb-veh { + background-image: url(/static/vid-expanded-hover.png); /* SPRITE */ +} /******** self text stuff ****/ +.subreddit .usertext .md { + padding: 2px 5px; + background-color: #fafafa; + border: 1px solid #CCC; + -moz-border-radius: 7px; + -webkit-border-radius: 7px; +} + .link .usertext .md { padding: 0 5px; background-color: #fafafa; @@ -2152,18 +2250,19 @@ ul.tabmenu.formtab { -webkit-border-radius: 7px; } -.usertext { + +.usertext { font-size: small; position: relative; } -.usertext-edit { +.usertext-edit { margin-top: 5px; padding: 0 1px; /* so the border of help/textbox don't get chopped off */ width: 500px; } -.usertext-edit textarea { +.usertext-edit textarea { width: 500px; height: 100px; } @@ -2181,23 +2280,23 @@ ul.tabmenu.formtab { display: inline-block; } -.usertext button { +.usertext button { margin: 5px 5px 10px 0; } -.usertext .help-toggle { +.usertext .help-toggle { font-size: smaller; float:right; margin-top: 5px; } -.usertext .bottom-area { +.usertext .bottom-area { /* this restricts children floats to the container */ overflow: hidden; width: 100%; } -.usertext table.markhelp { +.usertext table.markhelp { background-color: white; margin: 5px 0px; width: 100%; @@ -2232,7 +2331,7 @@ ul.tabmenu.formtab { vertical-align: top; } -.roundfield .usertext-edit { +.roundfield .usertext-edit { width: 500px; } @@ -2246,9 +2345,104 @@ ul.tabmenu.formtab { border: 1px solid gray; } -.roundfield.captcha .capimage { +.roundfield.captcha .capimage { margin-bottom: 10px; } +.roundfield label { font-size: smaller; padding-right: 2px; } + + + +/*** linefield stuff *****/ +.linefield { + width: 514px; + padding: 7px 5px; + font-size: large; +} + +.linefield .title { + background-color:#CEE3F8; + /*background-color:#EFF7FF;*/ + color: #336699; + font-variant:small-caps; + font-weight:bold; + letter-spacing:-0.02em; + margin:0 10px; + padding:1px 10px; +} +.linefield .title + .gray { + font-size: x-small; +} +.linefield .delete-field { + padding: 0; + font-size: smaller; +} + +.linefield span + span { + margin-left: 10px; +} +.linefield .info { + font-style: italic; + color: red; + font-size: small; +} + +.linefield .linefield-content { + border-color:#CEE3F8; + /*border-color:#EFF7FE;*/ + border-style:solid none none; + border-width:4px medium medium; + padding:5px 7px; + vertical-align:top; +} + +.linefield.usertext .usertext-edit { + font-size: small; +} + +.linefield.usertext .edit-usertext { + font-size: x-small; + float: right; +} + +.linefield .upload { + font-size: small; +} +.linefield .upload label { + font-size: small; +} + +.linefield.usertext .infobar { + width: 100%; +} + +.linefield.usertext .usertext-buttons { + display: none; +} + +.linefield textarea, +.linefield input[type=text], +.linefield input[type=password] { + font-size: 100%; + width: 492px; + padding: 3px; + margin: 0; + border: 1px solid gray; +} + +.linefield select { margin: 0; } + +.linefield + +.linefield.captcha .capimage { + margin-bottom: 10px; +} +.linefield label { font-size: smaller; padding-right: 2px; } +.linefield span{ font-size: smaller; } + +.linefield input[type="text"].small-text { + font-size: smaller; + width: 100%; +} /***traffic stuff***/ .traffic-table {margin: 10px 20px; } @@ -2267,11 +2461,601 @@ ul.tabmenu.formtab { .traffic-table tr.odd { background-color: #E0E0E0; } .traffic-table tr.mean { font-style: italic; border-top: 1px solid; } +.traffic-table .prelim { font-style: italic; } +.traffic-table .totals { font-style: italic; border-top: 1px solid black; } + .traffic-graph { padding: 10px; border: 1px solid #B0B0B0; margin-left: 10px; - margin-bottom: 10px; } + margin-bottom: 10px; +} + +.promoted-traffic h1 { + border: none; + margin-bottom: 10px; +} + +.promoted-traffic .usertable { margin-left: 0px; } + +.promoted-traffic h1 a { + font-size: small; + margin-left: 10px; +} + +.award-square-container { + max-width: 1000px; + overflow: hidden; +} + +.award-square { + float: left; + padding: 25px 0px 15px 40px; + white-space: nowrap; + width: 300px; +} + +.award-square img { + float: left; + margin: 0 10px; + width: 70px; + height: 70px; +} + +.award-square .award-name { + color: black; + font-size: 22px; + font-family: verdana, arial, helvetica, sans-serif; + font-weight: bold; + line-height: 1em; +} + +.award-square .winner-info { + line-height: 15px; + margin-top: 15px; + color: gray; +} + +.award-square .winner-name { + font-size: 18px; + color: #336699; +} + +.award-table { + margin: 5px; +} + +table.award-table { + margin: 5px 3px; +} + +.award-table th, .award-table td { + border: solid #cdcdcd 1px; + padding: 3px; +} + +.award-table th { + text-align: center; + font-weight: bold; +} + +.sponsorshipbox { + max-width: 300px; +} + +.sponsorshipbox span { + color: gray; +} + +.sponsorshipbox div { + border: 1px solid #D0D0D0; + width: 300px; +} + +/* otherwise the pixel will cause a horizontal scrollbar in firefox */ +.sponsorshipbox .promote-pixel { + right:0; +} + +.sidecontentbox a.helplink { + float: right; + font-size: x-small; + margin-top: 4px; +} + +.trophy-table { + width: 100%; +} + +.trophy-area .content { + background-color: #f5f5f5; +} + +.trophy-info { + text-align: center; + vertical-align: top; +} + +.trophy-info div { + margin-left: auto; + margin-right: auto; + width: 130px; + vertical-align: top; + padding: 15px 0 15px; +} + +.trophy-icon { + margin-bottom: 2px; + width: 40px; + height: 40px; +} + +.trophy-info.left { + margin-right: 10px; +} + +.trophy-info.right { +} + +.trophy-name { + color: black !important; +} + +.trophy-description { + color: #555555; + font-size: x-small; +} + +.dust { + text-align: center; + margin: 45px auto; + color: #d0d0d0; +} + +.removecup-button { + display: inline; +} + +/* Datepicker +----------------------------------*/ +.datepicker { + display: none; + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + } +.datepicker.inuse { display: block; } + +.ui-datepicker-inline { + font-size: x-small; + padding: 5px; +} +.ui-datepicker-inline .ui-datepicker-prev {float: left; } +.ui-datepicker-inline .ui-datepicker-next {float: right; } + +.ui-datepicker-inline .ui-datepicker-prev span, +.ui-datepicker-inline .ui-datepicker-next span { + display: block; + height: 1.5em; + width: 1.5em; + text-align: center; + color: black; + border: 1px solid #369; + margin-right: 1px; + margin-bottom: 1px; +} +.ui-datepicker-inline .ui-datepicker-prev:active, +.ui-datepicker-inline .ui-datepicker-next:active { + padding: 6px 4px 4px 6px; +} +.ui-datepicker-inline .ui-datepicker-prev.ui-state-disabled, +.ui-datepicker-inline .ui-datepicker-next.ui-state-disabled { + display: none; +} + +.ui-datepicker-inline .ui-datepicker-prev, +.ui-datepicker-inline .ui-datepicker-next { + display: block; + cursor: pointer; + padding: 5px; +} + +.ui-datepicker-inline .ui-datepicker-title {text-align: center; padding: 5px; } +.ui-datepicker-inline table { + clear: right; +} + +.ui-datepicker-inline .ui-datepicker-calendar th, +.ui-datepicker-inline .ui-datepicker-calendar td { + padding: 0px;} + +.ui-datepicker-inline .ui-datepicker-calendar th span, +.ui-datepicker-inline .ui-datepicker-calendar td span, +.ui-datepicker-inline .ui-datepicker-calendar td a { + border: 1px outset #888; + margin: 1px; + padding: 2px; + display: block; + width: 2em; + height: 2em; + text-align: right; + vertical-align: middle; + padding-right: 4px; + color: black; +} + +.ui-datepicker-inline .ui-datepicker-calendar th span { + text-align: center; + border: none; +} + +.ui-datepicker-inline .ui-datepicker-calendar td.ui-datepicker-today a, +.ui-datepicker-inline .ui-datepicker-calendar td.ui-datepicker-today span + { + border: 1px solid #DB8; + background-color: #FF8; + color: #888; +} + +.ui-datepicker-inline .ui-datepicker-calendar td span { + border-style: solid; + color: #888; + background-color: #EEE; +} + +.ui-datepicker-inline .ui-datepicker-calendar td a.ui-state-hover { + border-color: red; +} + +.ui-datepicker-inline .ui-datepicker-calendar td a.ui-state-active { + border: 1px solid red; + background-color: #FCC; + font-weight: bold; +} + +.date-input { + display: inline; + position: relative; +} +.date-input input { + border: 1px solid #888; + padding: 2px; + text-align: center; + margin: 0 2px; +} +.date-input .drop-choices { + position: absolute; + border: 1px solid #666; + background-color: white; + margin: 10px 3px; +} + + +.payment-setup input[name=bid] { width: 6em; text-align: right; } +.payment-setup form { margin: 20px; } +.payment-setup p { margin-bottom: 10px; } + +.pay-form textarea[disabled] { + font-size:smaller; + padding: 0; +} +.pay-form *[disabled], +.pay-form input[disabled] + { + border: none; + color: black; + font-weight: bold; + background-color: white; +} + +.bid-table { margin: 5px 10px; } +.bid-table td, +.bid-table th +{ + padding: 2px 5px; + text-align: right; +} + +.bid-table th +{ + text-align: center; + font-weight: bold; +} + +.create-promo { float: left; width: 520px; margin-right: 20px;} +.create-promo .infobar { + margin-right: 0; + border-color: red; + color: black; + background: none; +} +.create-promo h2 { margin-top: 10px; color: black; } +.create-promo ol { margin: 0px 30px 10px 30px; } +.create-promo ol > li { + list-style-type: disc; margin: +} +.create-promo .rules { text-align: right; } + +.create-reddit h1, +.create-promotion h1 { font-size: 200%; color: #999; xmargin:10px 0 0 5px; } +.create-promotion .sitetable { margin: 5px; } +.create-promotion .infobar { margin-left: 5px; } +.create-reddit h1 b { color: #666; } + +.create-promotion .create-promo .save-button { float:right; } + +.bidding-history { padding-top: 10px; } +.bidding-history .linefield { + width: auto; + overflow: hidden; + padding-left: 10px; + border-left: 1px #DDD dashed; } +.bidding-history .linefield .bid-table, +.bidding-history .linefield .notes { font-size: x-small; } +.bidding-history .linefield .notes { margin-top: 10px; } +.bidding-history .linefield .notes p { + text-indent: -20px; + padding-left: 20px; + margin-bottom: 2px; + font-family: courier; +} + + + + +.pay-form tr.input-error th { + color: red; + font-weight: bold; + font-style: italic; +} +.pay-form th { padding: 0px } + +.pay-form tr.input-error input, +.pay-form tr.input-error textarea, +.pay-form tr.input-error select { border: 1px solid red; } + +.pay-form input[name=expirationDate], +.pay-form input[name=cardCode] { width: 10ex; } + +.pay-form .optional { font-size: smaller; } +.pay-form .disabled .optional { display: none; } +.pay-form p.info { color: red; } +.pay-link { font-size: smaller; margin-left: 10px; } + +dt { margin-left: 10px; font-weight: bold; } +dd { margin-left: 20px; } + +.calendar { + white-space: nowrap; + overflow: hidden; + width: 100%; + position: relative; + padding-top: 120px; + padding-bottom: 100px; + margin: 20px 0; +} + +.calendar div.grid { + position: absolute; + top: 0px; + bottom: 0px; +} + +.calendar div.grid.today { + background-color: yellow; +} + +.calendar div.grid div.header { + font-weight: bold; + border-bottom: 2px solid gray; + text-align: center; +} + +.calendar div.grid + div.grid { + border-left: 1px dashed gray; +} + +.calendar .blob { + margin: 0; + padding: 0px; + z-index: 10; + float: left; + cursor: pointer; +} + +.calendar .blob.link:hover { + border: 1px solid red; + box-shadow: 2px 2px 2px #000; + -moz-box-shadow: 2px 2px 2px #000; + z-index: 100; +} + +.calendar .blob .bid { + text-align: right; + font-weight: bold; + padding: 5px 5px 0px 5px; ; +} + +.calendar .blob.link { + margin: -1px -2px 0px -2px; + border: 1px solid black; +} + +.calendar .blob.link .title{ + font-size: small; + padding: 0 5px 5px 5px; + display: block; +} + +.borderless td { + border: none; +} + +/* title box */ +.titlebox { + font-size: larger; +} + +.titlebox h1 { + font-family:arial,verdana,helvetica,sans-serif; + margin: 0px; + margin-bottom: 5px; + font-weight: bold; + font-size: 19px; +} + +.titlebox .karma { + font-size: 18px; + font-weight: bold; +} + +.titlebox .fancy-toggle-button { + display: inline-block; + margin-right: 5px; +} + +.titlebox .bottom { + border-top: 1px solid gray; + padding-top: 2px; + font-size: 80%; + color: gray; +} + +.titlebox .age {float: right;} +.titlebox .md { font-size: 90%; } +.titlebox .account-notes { + font-weight: normal; + font-size: small; + margin-left: 5px; +} + +.titlebox .account-notes .unusual { + background-color: #ffdddd; + border: solid red 1px; + padding: 1px 2px 2px; + margin-left: 5px; +} + +.sidecontentbox { + font-size: normal; + } + +.sidecontentbox .content { + margin: 0; + padding: 5px; + border: 1px solid gray; + font-size: larger; +} + +.sidecontentbox h1 { + text-transform:uppercase; + margin: 0; + color: gray; + font-size: 130%; +} + +.sidecontentbox .author { + display: block; +} + +.titlebox form.toggle { + margin: 5px 0; + padding-left: 20px; + font-size: smaller; + color: gray; + background: white none no-repeat scroll center left; +} + +.titlebox form.leavemoderator-button { + background-image: url(/static/star.png); /* SPRITE */ +} +.titlebox form.leavecontributor-button { + background-image: url(/static/pencil.png); /* SPRITE */ +} + +.icon-menu a { + padding-left: 20px; + background: white none no-repeat scroll center left; +} +.icon-menu li {margin: 5px 0;} + +.icon-menu .reddit-edit { + background-image: url(/static/reddit_edit.png); /* SPRITE */ +} +.icon-menu .reddit-traffic { + background-image: url(/static/reddit_traffic.png); /* SPRITE */ +} +.icon-menu .reddit-reported { + background-image: url(/static/reddit_reported.png); /* SPRITE */ +} +.icon-menu .reddit-spam { + background-image: url(/static/reddit_spam.png); /* SPRITE */ +} +.icon-menu .reddit-ban { + background-image: url(/static/reddit_ban.png); /* SPRITE */ +} +.icon-menu .reddit-moderators { + background-image: url(/static/star.png); /* SPRITE */ +} +.icon-menu .reddit-contributors { + background-image: url(/static/pencil.png); /* SPRITE */ +} + +.linkinfo { + padding: 5px; + border: 1px solid #5f99cf; + background-color: #EFF7FF; + font-family:arial,helvetica,sans-serif; + font-size: larger; + -moz-border-radius:3px; + -webkit-border-radius:3px; +} + +.linkinfo .score .number { + font-size: 22px; + font-weight: bold; + } + +.linkinfo .score .word { + font-size: 15px; + font-weight: bold; + } + + +.linkinfo .upvotes {font-size: 80%; color: orangered;} +.linkinfo .downvotes {font-size: 80%; color: #5f99cf; } + +.linkinfo table {margin-top: 5px;} + +.linkinfo td, .linkinfo th { + padding: 2px; + font-size: smaller; + border: 1px solid gray; +} + +a.ip { + border: solid 1px #eeeeee; + color: #cdcdcd; + font-family: monospace; + text-size: x-small; +} + +a.ip:hover { + text-decoration: none; + color: orangered; + border: solid 1px orangered; +} + +.lined-table, .lined-table th, .lined-table td { + border: solid #cdcdcd 1px; + border-collapse: collapse; + padding: 2px; + margin-bottom: 10px; +} + +.lined-table th { + font-weight: bold; +} + diff --git a/r2/r2/public/static/css/spreadshirt.css b/r2/r2/public/static/css/spreadshirt.css index 736ae1f44..e8d53cd04 100644 --- a/r2/r2/public/static/css/spreadshirt.css +++ b/r2/r2/public/static/css/spreadshirt.css @@ -1,12 +1,16 @@ .shirt { margin-left: 30px; - width: 600px; } -.shirt h2 { - margin: 30px; - text-align: center; - font-size: large; - color: gray; +.shirt p { + font-family: arial; + font-size: larger; + color: #111; +} +.shirt h2 { + font-family: arial; + color:gray; + font-size:x-large; + font-weight:normal; } .shirt .shirt-container { margin: 10px; } .shirt .shirt-container .left { diff --git a/r2/r2/public/static/dragonage/bgfinal.jpg b/r2/r2/public/static/dragonage/bgfinal.jpg new file mode 100644 index 0000000000000000000000000000000000000000..db962d2a83daa932d2331ff09c07163d02d8a387 GIT binary patch literal 218450 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¿Vo7l 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
~ zb4~h@Rj#K!K3ZUhEB}t{6bHEPQsk}wc|_;+PrRnvReJ@hPH=OlRQ)|wOdn!#dB;*! z2x=9;-EUI#nDebD#V3t~0F9qX^<6Hz|i&d~ms z7ZLZ3(HsK^4Ju@tV+*oWVa#`Ih2-reEk_;LhX~`6n~GiBm5 zTY9-M?&*WVOoO~7k>&gaML{!dmI0ktd^7vo-`o!R7fZv`Wrx6PEm;7^}km$yoo>KFlltD z;x}>Ku+d~4|6Xee)2;9X7FE##g@-uZq#)jRpJ&0b`EpL$6>)XLJM6i|>K$aW_Q~-_ z*=p3k_;**RxqaQIOoa0n%T|z0F-9y{0!xK-Af5*atEHJ_emJOxP)WDQ+HV#`wUGo4 zH9V7xpKaKb!laC7IrCnTO5WPwyRj8*dO}0mRWlq@V;Nqz#CUiSMO+w_i~vPGPFa&q zdWYEUZ@;AFr64Z1&SrAx`tA;Pj5h44M-I~`ZU-FYs13$>Uo1T12>kA{B5Y4<)9+I5 z;e(1vNVy%p#_b*xh{0IDt0dMfZo}FtIDG*(Se$OW)YJHoIBJ+U(_t{;`Ewnym$Cgb ztx_Nb_in4{dbk(+o@=|CLWM_tgVSx)7gi$mDSB@j*2p`o@$*O!Vc(GQt&P!*sCixu z(+UJMUDsuPQKb?s>#baA=rHT5ar^!VgDJbf9@TS~u_DEVbtVVc22xNVnr5)As*W9O zXY5ZLS`P2qS>m($BseV4wE-<5r?DyW+_4H*LNMDMSF|fE3Z^5C0{}-bBciU_1@?oR zT}GD$oO9Lk#M337;V(d~onWHRNHTyU(!D7ikK9J`I*XrBjgr;p9&T=;!vi{IP4+{r zs@b(vdrMOd#dJ_0J0f#nF!_#$Fx!%*mCcb{P1)8tVw|J&X+?E-vfeiPda;t_x5)kA zM_LP{RY(t`rY3h_5R=tq+UG4OF3Swh-~nq1O@ab25W99ZN2NVM#ymgDR4 zUl@oSk+3#rt<=(@DfM=5{1#a^(POEms8M)HzaV_Mu;Q(#UBAaC{w9LkHyi&!NvT2-DN@{4eV>lSHO$4BnDV6((56^`@? z$0eaUz>u!?2BXOn3c4 z?AP+%Q_Dq(oVKlr`drS=%u|Y@!{5x=e++8(JC#7L_6jYAU@7)WQ4J89w##Wdsyuj% zxPF9A(Kj`$tIG+oRY{H(tESVYng0{d&5Qd*AmH7&j zxGCmLHGq*LOI$JjnJcj516A4dwT^NrrZd_nG%kfk_J-jXO$ZzcGVF{-VWg{ZDD7Cy zYFF9J1y_+5?<;hb3wBlkhM1s06m4q^dt(;N%`GtGXkZ^)$;L$*MbT|<&FSqn`vV-b z__4izN&jWcRa`iUOH6@4_Ws?E1Z`2*NOuw{DTSdGi-ID7iBcFHV;Jqh-KKr7i?GImms>_Ase6B67zg0q`B3Arqa zOS3EKApZki3A&{uhynAYl&+IEYr|bowr1Gs<>#U~J>u2TeW#o+BaL9Lj3y$QbN6F+R*`r+W+I?!h5C2CaJEzPYbLS`jLMgylu7 zkCN#kivizZwm(>-MKNs6d%@CKS&-bUcdwZ>UX+b{#$|XP;6{M=oqjd6@F*B^MfWGw zj9NIDo>Om}Ir6m9&(P)HQ`y2Ad|cSdIXtiy3peXV7Bh{Nfb`@S?#ri$0#kQ%ThW^?)Q z*E&q+Rd0aT0FxeWPE3fBWJeVqF;b|}?M(T20)hA50NTS(qhU?4P!y*}0M^8F44N}t zWRw@o>28dfV`{Hhkt1i#u&%=y{4nOB8?G}!XnBLZcH6@ON53Z9ZB(!c{T3seHG#VP zBJEkbZD+0?W%wUMfUmW@i1aPYcNSV>Ly4?D_%q`*)_Bu<}p&ZU<6k*ExD^ox=~TSPexFQ zKP?!KIq#HQ9Yw&ebbZ_m#l*!u-;g{4;?IEOV#Vj>4p)jlf0N!ve@Pt)nU#XvB-csc zP8YiKLt>qgTQ+MK7f+-<;{%hETJ|imds6$?m|8a$-VQPwK}IQtN+codyS!5Lhc^?# ze*F+V5?A+=SArjauXKAKhQ|KTxAeLfpSjN}=0RyU)||6Y`kHj#upC`*B@VMo%DD5E zt-g&?d;}y5PbD4oL$3G7vM%-?{Edy}_VgM$jyvDq_1f~%noO16c;TIM_?JZ9O0(t< zzNdeCTz*MsRTD70yyIbzmBRn-5y1TjfXzKj9_@y{dpl=n3%cG&5^!g@L(>1Lb!qob zx^{7I-6Fj7NgG>B%D|yk->P3GvGbOgDG5K5C7dk>BzFyDqsW3sc?ipi**&yB8$yn! zN-sud%rg8lTke>&radtD2i6@d1Kf$h$+$@ei99{Fofw*z(r5t2ObpZYVsY5Hpr}b< z$OHA9ys#0;wP*TVSp%r)2Zm)-O_Y3W-M~VxHCfxYi430xg4Q}mZUd)ZMjkFD ziQ5l2!e{4>&_BhB(nq?3p4%Zw_fxQAZ*J^7F7DcjA1usM)@QF7Gj9(-4mjCcl_5%*OYWQ8=W2CN1F?yU^va>j6r_K;N(ym>S)#+s-8I+F zr39H`=9(T*&=s0G)hosBC&sT< z^rmT~==R!UxwcZR`N24;T6UgiEe=ah(C04cYF4;f?VVgwUkF$(fT1QR+b@^M27c_R z`j*48Kc9{d8{FhZZ<^9m*DGVkRXRM~sELuBTmlZF&lmqB?%F&8B0*z+in4y~DBWts zej$|wki39cbLtaLYfU>z})aqV~t%Qlw$x_Euc@tM{S!((CutOveE+OJ`nq)ABgME!U>!7nb@G@&T{Y; zb~<^=JLq~r;pMmkh1^2+aL7c{#VqFhJ!r0xNC%G5wUPBJQs3l~S}(U|AN}v{dt8AU z{juoQNH*-H%@qm?k;ANao)Xi=f*Ik3 zec{#?G1j9jA*ldN9R(frzAUo2>vkl_ z?vx#cNy%Ax=ldoRH8c0u^dQqFxL-SZ6euR|lGgUPtX%u$H(=Qimd*y!!3ETQFn`dK z$H4Y;Q2&I<*ghtq4b zz9g~(wfC_?P2$Zf$p^rA)z8N5mlSQdV4Q|E(*zMq;Zkgx&KX#fA6%hoG3 zT!(KY+j}fSd-f#^D%+X1wV5~CQmbVVuTM$+n#b^LYrFW#21HuX=a8Vq-r)j9NufrL8CnUGrO{;lx*Pw_=`#t9ef+ zuFA+bv1lS)$GI)fV|!X-lEGv8lzqz&c5Aj{_Y0c{w(IC8xXTcJGrvmV`TCrtqNve7 z7{$Fn&;9J3nIl^-#ZyE6z_Bkev@XkBE1xM8a?sG4sVB0)y3`ot)-A%Q78WWPCF>AFsa8ae7MMTrg^ zZAaAcFIMDyT*(qER3)*ShVX#hKQaZgf34h6u#L&jyTDVQo=%6BTMyOj7|n6gyV`$X zBK-WV(aEjQHfJ4vSg#@H=BU25Mfuhb7Cj6e`(Q_{x`$-_&2qmGO+Pzdsr=O&Id9>V z+BaYU5g4InsFHcwXSpMb*CwZ>PQUF z($21Sr0L*GN08X_mMDR8Vx()2vh3iCqd5qd!>eCLm8^;szZ*qCCx|6t`KAY*8niDu zB~|_{Hc0MkDcKk1khqu?Q#T;0KtzDP>+b_`Ni`eLzIGwK=&%brI8}I!CMCB-{=oR5 zFA)l*<9bC*ycMU)$JQB~L^4bGk-dz@QMTveWI|pz=zO7~HWGyXXH690V+hNmtbAvM zW)%`T3&PXfJ(x25T{$;>QEdf=!hqS;VLvuNU27(rq^h6Wm}So+4&&t^%c;|j>iYr8 z)_yJfg^f%jTc)?}A>*}Q?XnMtg%dy~9E=Ub!U}5;x4fx&$(EGqQG@MIVw_!!dQd@t zVm%7%ZC40dm~AQ35t3Sgr_Y5ob;=P72Wr+;dh_R1{lmJ5-RLU1$d1iD^LHSFazpO| zvnyAvX4xYi8D+FKp%GW;$a!O_KD_6HT?K#F>B1^mXHa{@0Q<#lUs=hRD|InJ&<_Xx z=q69I_%MC@hKOV?;)|43t!^Gt52;zZP*G0gsb1ek6%iBA`TM%Oy0Zsba=vW#){l@BdWl*fKBw>g4> zi?sUNQM;~Ri>^IJx5AEfwdi=*K9Xpp>Z6VzYWpZ%NO#K=w=10JL>nWG}`h7$rg0tj?8II(d4s zHmU(#KnZLA_35q9mI2kolEDAN*;z!z)dcFcu>^NWg1bxb1b266T!L#uaF^ij7A&|o z+Kp>)*Tx#RpiOW|!hgAM@CI-A#c4s;|Dit6+2lLY*fY3riyXjX&W7P_o6^ zQK}3nRhL6D91K1DMjHhyLfI~-hZk@2o=yzJV4V{S%+sV;xUrl9;|qIfsK+aEa+$u0 zhFa8wPzxy!wpU|e?bO1 z0<@C7iF-z`Br?~UcUzI zd)$3ob_$n$Zt46F@Wjjno7d}gkQj!@+4_m=@KM-LHCoX;g3dW#7vQLL(Bq;9aSj+W zyuMQlC@m_w2FjXQ2CY3XYA}r)eCr&uSl*|rCNxpzvQD15o|7m4LR<~M3b>1G_kS)_)bfMoIT~W{#5TkMLHqdweCwcwHziEZ@lcU#d{k^z;Vle895P9Oy z{lyP)k-IHQR6)(3GvmjIg@y+IpcwBB8?h*SpE|NW*bdsW1+Nitqj^q^A!1*4n%3{efS)+V)v$S^^#M%!} z8js(*Xh2D(E;YRE4)}3HMtwxRIJaf=8-H`~LPf3U4|{90_7nRbWwTU= z$n^7&sWSiCGrWB`g*Q88aymgc#8u#YGuyL;rUXD3O2=(c$9L$b>#0zCvO)G6-V#vv z?ES#)>cJLR@LZFepZh6=B#EUw73k>b|K_z{MOHaa`JK1o;gi{*)^1_ln$0KE%W{fQ z61V(OHHtp&z-G3*08P<>BmPW*$dLn=f6-B0;;j=-)w>NECWRUk_q`2jq6b5y?1ggU zMAY`fIgMDfMv+Mb^WPOh0i>{;C}x z+a5Ldsm+?Tu~%? zrzSl_CVluO`EEIepiUcvfdXaaa|Pf#I;K=5hFn5nZiwTD5@`&g8PvQmNgUL~d!=*9 zX8}@(y@OG!5s&Yu%f>dsQK2*dZz++UW#-${2Pd&B2Za4u8OaBFLk5D8z~L$!7Jk&n ziw!gh@%n(4Pb)IZeG{95g`D4;JpKc8Zq>3>@e|VChs4)$9p*Bk-{Kb8y|T=JSz5M1 z3okxp8iPF_z9s+-kfDk5cPOC2ELWo#s6BslX=+ZHx|8+V6}t_LVnSe?eAPe7b&9MR zA^}QANl&SXPy89_@aW544Wcfy3Te-RG`msqztC^_#TjsdmeM9_wWOyn_$yr(j< z)Y2d&S-a@!&L#3$(g}mR=2unH$&)oTEngEn%qiI}Y`c4UA~@@vxjAXWY_Hu3TI3O@ z%8j(jSr)6|TKAr2S>QY)SU!arkQ-xKT1qI>DQo_eId*8fHWjq?4t$+DdyEtPx%oDa z>{?CK9l^jlu3p)YDiiO~#Dn6IH51M$tuZYd9iu3D;*;C6hJ_Y5V_w^^r`@#wKazIh z*3Aa@#f_<-XzQb2&sNWlwSF%s%LaD7F^5XO>kznj@^ZlQq)f}e2JHaCzGUDBvPC!; z+7Gv<4y`bz?tsoMLs2;AFFrgk%uFAR{H9afRu(x0uoX%ko~Z+siJDH59_oa$qL7W zJZ;w3COMZ{wW=|47U9>L+ z7ZZkTb#n#H)moYpTU7sS+D}^rT9vs;kMbMdrA7r-xCch@o`{>hEF88e94fLH;GHo# zb@f8LBALZZdepF%&IP&I-{Tq2KTh@6`?KcRNt)4&Y72!E5FhSUVDlN2W{`RDw# zD3p_u-x^S&nkA_5j$~L)nx?bLn)a}>v9Yof8Fb^Q(UXL+AtM-Y;SO*En9$Ps%$aVc zr?o)O17oZ27RAB2RS1vOSHbi>JoQmv6U86!o<9+iyuxWBFIWi}p-<6S8ktN4zar58F)e6DkEs+MhY=E|CcXBrImPlbrId)YKXVOwz%*p1e$ z?CiQrJbx=DUI~=x(DZi{kJTyk&ABQhV*-ZDJ%LMNEWryf4VgOFrmnyooC))=rMXRu zz4q}!O#I-}dyAWkWf42oZ>DKPH*9c2Hn`+l`!g^}p6$E-T0_<8F9fmdY@{*&%5@+= ziWZGrKCZHyvTXrXfsa03Gy(h%cosavADa$a8kOi(u=ON2)I5;bCaPG9QaAWA$r7^S z@~+%OtQBSQw#;n&__I>BdZ5rS7T~uS9oj2;s8^ww*8FTwZyhI1Skn4GhMR0|e@318 z?&Sv9icw8zWS971Uh*}UR@8*iCicZ4tli7-0zolOPHUklB|eR9?Cea{X|dnF-_B)a z^$MW4aJp!*C}V_HG3Xb4?YUhHBTIcKc_J5?LR->l9_8ZTpZVZe?yz`^mmc}&1aO{=0hxCaw$INwK-VIBi{DY2jGW!5&& zA!_oSt1_4#I%OjBbrLOoR4*oMLWv(a9A4+XP8zL1DFBiU()_{bTF^SY@9M(g7PdX( zTz@#?{7pd-{`uuV0LWT_LL3s8q7^I^6z&aPnP zZ}wbLx2W7h;|y&4mN!D{PbIM(Hc|pTOJp}beEwi-3eN6eS<*wRo_oX*i_u_=$bJV1 z5p?A}wJ>c~A0BH+9uo?GC9*Y7Uz*s8tBN$CA_VHsKF7j$!f#p&te+yi z5@SC;1)(=UsCG{3V+!}*Bmuwdyu%D;e9#{~T?h-cwyl-L#Ek~|_{<}9Ycdq+)%5$& z+&@D=RvRSwply?)0Uw(aBq}1N=6 zTI$ew8=8FlG!*Ou`SKFC=hiRp+;z@VU!krlsWc(%+4r0HHH~k|H5wP!E*9_sB&diD z;XASFMM)*0n69*U@cEP<%dgvcR4?YiijuK~y1Ctnt^El354w^Qf$iUlHPzIQC>`17 z?w47a4%TO76!tr;GMWud!uscQdgtDT1Vc2MW*tY$mcPGxT2NHK8am#5Au~xq{p~$cm_!nCK!Z+J*xF{P_OHO*MW%#%zlmpt= z6VaB_QQPb3!o&G$Q`X}p9v__vRMXoQ5(`W_Wwq}c}OpXm9(}Bk~NDS zkxGwyQ`9<6dX^pf+RdpGXRkFr?VcB^_&U09S5N^VGV3!-nn9cD)iwL(e0&Yd2SzMC zm$IBIY6&fwF5C_1#%HjGA*mB$tKRijQ=D+5Le_Y1e2Fl3Za0T_xe@f@8T?2u zf4hz2@v}+DZ(~c$a@P8QJy9w2X1{e^h?LSh%B3sdYUGys zx)9tk>Bn9}*thyqaIH+@;HjRgOa9oHu{{9vyNWDpfHpP_0th$Dyw~3@TT{YAtWS(m zRda}2FD6d%&>-lG;WE33W%cP6AP|+Hy*)Ea@3l<`1lm_ASS`XAdn2_AinKTDf3n4H zT1gvblEus1S6^0|;=a?iY1$|EwX!dw{&-#)z^NKAD#vQpl|7_mIO$>*`BRzTxZ%)9 zC5G!WXXfZn8v|R`Ju+V1l^4>A3-Qp@lYf#kuO=R3At6imoP;P=#_Pi8Wha}oO!!f; z+R}r|f}`%cxQSLhC)xR%E*N>RSL`9*o*(qgs6VL>L-W)JGzJdMmu= zwai&7UP`5Rg&4kGytpUU`gO>_bxraOorb5_wjnw?Na+1?fl_TB@1k96W(cYBBuN=* z)41?cO?}d&5kN-g?6S`O`~v;XV}-;`Z^byxE5yNEq|i!dB|uSBgxUvI4<;0MJ%VmL zc9G7Aef=)i4(U$BNE8}`IHxMVq-7G*{}C77J1ep)K2_plTOon_zmwYkkAFH(W1sX>=oHOSNp_%aCKW zpw5+K<%MzGn}5dQsvAxSE%M-zzGPnhQ3nwH2Sz42Zvj*?vcGhN!guM;k1>jk>vBmv z+S0^RG9qsU2;N>@+n&h2P9R$aelwJTZN?B$N}@n1kj37K}9a3iwH1=8g+TI#E= zBR3p7C{nJ6v=|S@=yZ(G9P!59)YlVarn|^`3$33aNG{ZLbXqWQE0}%2zl!u7|H05^3$g*wlzwY#6<%Fx*QKC2)dtf(p09*jFS}j zLq1#n2k7Q3PT#dcArs+-)Y|hkFHQHz|1{bA3_8DJRS^x3|06`d%QgKxjOxkJ%xWpN zuUJ#9sS5rV&D-*gd)}w@pgkeC+e5xs4sg-e4pIqbgav|q*gh9>W(|ob&WYZ$?UG*M zr(lq*?*v6ZP`!{j_Q$w?P-NROsfC$k*jTK7s}L+Fqq37InDE!<_zXAC4Dj2H`jC|Kb_)p%ZdYYI(!E>#o;fOj#S61ixw;5 zrjn&vpyEd5kY`1;ZWQ4ymrH8kgJQY*m0o`eXs=@>z4shscHCe5O}=RZA$HZ+$+ z8!3^i>$;TZgo(257tyTz;~9V$7@r@^Wvf&Nn+ZxI8Gx6@>G1=?;0mzn){6EETw5}p z@9#~mdE0g;pB~(E#sIqPAhREOaaHo(Ej_RCVU|eJu0fiUj2HR zmzak>ChAu%%dok-Oh9HB7h9jmr-rv#a5%f` zAofSV#|Yiyp@6*hIRT^kWF4HD-@z863_OQ!+=S7qT$=9^YgcQ*Qf~((6=$)#JGW`k z_cgma^t}u&vY4``X>PSk!MGK#5SZ{9Y=TKlInCj-^G9Qp!g z?}6{N!Oho)%FD|#Y`*LINU4F8^%kuP{!ea8Nzl#jx|J-s(NsOCo8yanpR4~vNJbo?b{Zd_y)y%}EW(Z`H^ViE&p0Un0>cNB->>Dax5>LEX zyJEdX`eulRUQy8NQfwr1>gkTYL(|zrtl1-@vv~n?Wj-lpvfTMgzXd2dD|IthLs#PP z-c^0z8G28K@j*9N8v+*V6e7He&CmK&*yg?OP(>$O{opg-f%Qcvb+iXqzXK@zQKy0D zYUJ2f;@vozHd4QRsfk&*XE1wsX3A)d!(+bXd-_*@`Mh_QhyCtuaU&8NB`DgKVN*)k zlgjyv{&9V56~mxHKqMRS)=yD2TTKZ@Pi9SbwgtEQQ*Q8bPu{F@@@%=2exshb=A2rD zuFq%kuaYI{Zj`}A@2N^p7js8SJm|1;N2RySrzA7}F{a{3oorC!kf-eUt|iD`>HGrq zSK4I>ufUbRY3(k`A8^)Lx*IIa1iK(BKIV^qu+p%wdJ0|Gh{ir{_GbGN?2q=X?dIzu-%DZo!`=WdHE;4jd)1 zcZ|ajC87gP@0E^1f%3O&G(xVn6l{2O1UhIy)Vbd`|i!nOT3M*zjwkgKY(6Q@)*!*h2s84;cyPc z-d&GqYc?rrm+Bb?M9}EyN&W}0Zfovjpz-2J9VAy_XOj449bGF|~1 z(_p0En^~=A%D3KvSDkzmL^?C-6GBZ&WA&3=?k1_JWz5kKnC)hRoWZFP+}Qf&;=pGy zw^qJVfA`gLN*A6v;*lglSYb@WYuvBuL@(R&lf~gagZFNkJK50rtfA%rZZzTGsiv_d zJ(`ox%8pa_B@E0anMp#uTzpn`)$s0$tGjFTq~-=FTOmX{<%cB`IgC|%Gf&?ZSHTbP zv6?5$Je@9ftF`+`dgr`U=MrwU_mX1h_Qcsi&DN=Od9Hs%h64v@nGR(4^Z9u`!#B|U zPonUsxmnj`JGdE~6K^)cNC|!$mr}=Ck(37=l@)eg03*I4vVeMz)z*$a$4kX$m(+_D zs|z1CX40}O9TuxD%x&1K({=>X>jH9*NYap5O1-GRp6mVxh;THYx}{bxX*3@7!kJW0 z{6%aN8SOiEiWv^yu7>D#o-r_%K^ZAC@a^G|q^0mnm{2i>qXC%HrX#|LhLs5U*n8Pf z!8N{z{w);EE3vQ{1S~^MG2AV66-#*FG!4C7_r4Wzv}Xx$P-$zdQe2V9{5y5+QJ0us zkvf}TLHNgeqQU4f7l)m3RrQ{$Na242TUgE{-8ui7=|n(^nncktD~m#Y$ae{~imCPK zsjHMpfg@pl=VYV*0A5-eu&Up22^Y6#*<#eE3LPh+xuXj$kuHVXQ`ZR&6qt^c?l`CA zj_6lxfsO)WaJz-&%#jjd?{9_Ldwd{h#$0EUMvGiA{VRy1-+uE?MIbmsV7qcdZp_<+ zBdM5V#R2QpAVPL*v`lcYc`*UDff@!q=jvDfWjdp^@tFRMw)n-DO2E*?1GlffGbJd` z>2>DJNX-E?SXBE0vwCoNo--fspCrt8k7TyZTNRbOD5S}K=M&4ge0YnQ9b0gJwSn9r znWas)x=?WJ=1WE87Nb~G(y}F7Hui%pO6!3Dzl7p^(l+mXa>2Kk^^?2R7Z$LY(&6?b zS=tBdjf&6|4I|x0uay?F=wGTKpa2dhgCBR_+VJ~Pw!tDvFJ$vTUeg@tzk_ruZaw+Z zKuA%pmjnu-aDeB9Zp{u!dECBBhUK}g;2!55d4OgFiUAv6XGcv-ImSJFhgjFUHMxji79qLgG=2n`P5vQ`$YD(BH{-ejDF|BVpH`U9Dm#h zPinowomG5!n7~=rYQTw-azr6B4ww8XTMJ`zJKxNkothr?`Msrc0j@cqTbCG~tkg_h zvrpKyvu7`z57o~dpJncz!4a>>fBXIaU`C1*#4q?{k;6HYhY7#MXycis+r-} z*CZPfRyyj4CC!Joy7@%1og_@7rtGL6l_?72n`&21ujX{=rlW5ENMz=2PeXH;mu*pI zzN2$UG*?8leiKYBx!#DPUf`l@<^5?+8}eM`42;f)Q6Dmz zC>#03`$8g`{sZ7fm^u}6TIKcaUt@LKBt{;tT0jo0_)shF^uKDIb!Nmf8-ahJb#kB!x{96&GbK!o#70P**Z_h9mZc4- z;_A+INr?d5P}lf`9nhD@n7G+FNum)1eJo`NPD}9|U$bm}@ZsH@eU(sHRA$NB@3web z$G%^pv14cshkJ*$POMb@cJI@q`HtZ9kep<}Jl90-$JC0prB<9{H0DZh-zxQwVtH?A z*hT8}9*Zm>nCdx)s!Uvgd*X@zOo)C$U@9PUorD;bdClBC86Y5qbB7BwE^`ArY<32N z2#uxAFPn3WGy4Y3^2q0;Y)}FxO;k*RXd!}O zf~zsJ9=#zG8OEC%!*Pw^z<4<<#4#~|AGwyMu5LKSCbCzEu{`s+i)bm$)815<^i#(Q z`=Au%4{FHpdX;pVIS2F6A?F@qYG`p4LWa@o%sE>{vGs>4XR5rOnbA4)!fyxJ%N8-o zmg#vvM-~7n4$F`m5IoLMl`xXTYkM`RsD0wsBN35d<5aSOAOk2p@!V)Oy3(y4w-h2c z9K6EEm56_AIUTLP-!yqr7&dkVvG97WNPl*D_25$NSH8)M_IBH_=F(o6AZ_pZ9yW^Y zXqb~O2_QppJHZ}?HB`RjfC>l9oL$6 znLLr|!2`&;wvX{>{}|79OK{ljHJR)Hf3YAx$WHnd#XT~q>)2PCbG;~&V&2R;eib>} z$j01XvAkf|##eH2AuJPP>0ih=EBoT-vlPsexo1+zOmflj{%kUq%h@GUBbz0)W=S0N zbN#jj8EFL+LKlxSqwvA)=Dld*4DW8D0lW!)D8w*;QkQT&ZT?+tVcj}wlIW6$miJpv zOox9po)Tu|#q5%aE45o-Q+T}ZwRihGq;_K!K1w`Q*<-5jg_msB8DOBKm!$Fv09;?| zclWgelteQC-l<)IoUpR)7)_EjS!E_1GGUa-r*vDrWT6u6Y^#^k`JrkN>$ zK^N^BGj}}FEJfgzcJTQ-XP$(A8>_Wpk(`lN`UM{~Q4QGWR1HkumkoU~KzvZlGW_t) zRc^hAYWtOWdJ~%`*{SQqLC~G%PrV`_@ZRy6=R;@H zp5Iy5XNE+^>t9i8W}lJUZ8qxI*aQl0SwD+0Pds+#oNF+R?Oqd!p2a1&!FjXJC(|R> zxZ;<&6P*}{aTe=K|?Hz9w_?yz1~WnNGGz0Ez!w6-q- zmbm|zN?0}jNd)-(pTQN4)$`tcNy?X%9_d$jcky0?yZto*g7S9$4a`(k7`$n$1nYhW56Xmi5x8@N{Ab24Q zPzcQ^K%mCxgFw|yqJKwj_GcShM*}!HUkuZFvGV0{gZX)`%Y%bZ6=-f7myDVBF!ID*{7>)OO3o7Lq7IP=AUPx7NM%~=VtSlV?RQGw0W6l}{_YH{O;8uYr7yh* z<8WL~-GFcO2d5K}P-i%DK*+_`>SFQRI-(8~EkHk6$(SF6$v!?O#gc0+#daKIeca5$ z^2~a^GGHHcEIw93`eq3vNuiv=(#y@ZnOkmmKI=H{9&MbslM5)3m4eb3e?hy`OoSM?Hva;Ln&|+A2YpiO` z^oNLa>!SSjU961s`ei%Vy6$NQe>SfcmuIT4fc<$@K7EhaQX;-qtdam?rWE&b$AIw; zmNV2CkG|e;o3u5XSJY}aI~s`z?qW^5r*GI4?C#~e)21E}(J;xkCb(Q%QnE9YfhgtY zbxd7NKOEY4Ly3D{jR;r&0g%)|Q+B_y=Y^$Ldq3NfZljGBd8-&Z**~C9%2zsMC8&I) zR9Y6AbXY7tcIy!#FY(q0JgX-7m9uZK$~&?k9}nTgFpsYy=702>spV{e_o`)ry`mk< zu?EfiId9?C1_}M8Wl2Mvjvu^}ekhT7)~6rE9v(ckBIcK;|I!i&WnHU%Wst*WP+aBV zul&j0U|E1Sdp@e?J%&U?ITVyBvv_@Gqa)-ljbI_dOGT!fT3$$*QPYiqzY6d=DjmUf z$}_S&sXsQC6!DKFWDAlpNKzJ znMH)({s#z`{K?ON8~s&mv`aR&0x3?e$UHVs_;#KanzkHxmbp)dZ@BCHfeww|kUnU|8)$4YDPMi=@0&=-sN##=(BpDPh?sW%*k0=_ zN%Kl?xh5_ZM$`%=#gc*J_*)Gr%5T3HDg-}?wrC}!6HZia;HHAzMjEQwxyQ*B_Z8!| zxaCs>fYBbBUQ*DpsUh#&9}i3Ow2)n*G}0n!6tmnn7?E=qRx)?gvqfJi68l+;E!KL9 zj?pp2@uiqI_lty4{pSdmD63b4O;PM=9aD<_Nhr;|eZP?m)!ElLnPWkBEUI}ehHE<& zF|f-IR@0wR21Mr-=$96@BT}W~H)r&OF6p1tfbZfB;B(Z>x(gr7CE(SSSH! zaAhQeQvi0JBo%X?=|^WDR}ju-cU>C><&<>E2XO+rr0oW3xPYU{1~y z=G||WJ{yPmeMn6wt3LN0dAW0V9f-bkF08bB*BCpvoGg%R&Hed#4nZ# zuJPBe4ANAj^B_Hnj7wvYEJh1TKtC&DkSoKG=a5JIkpu@EV|?OwVS5b>SDu49Q3ulr z&`ylJPg#xr{L3x+Y?A<@#UMUK!x8@xTp^7nB@JT=D?sBHM6$NeWwB27S5KOlu-D31 zbi%W^KV_^iQO439qTF;^3VoB;m?=BaITwYB=Msxko)_x!?{K_L=XK5IPRIHm;5NRh zi%g-LQ*6^EPez3)gF8N$}D*BL%wM@*xxo;fEhjAdEs z5bM=?Rx>32#*Jv)R6|a4-;aJZVR^T`^S;>y6R&YZ{vxp6+R}@lO%n--d_!KHp$Lt8 z>vN_Cog-Ab!cz*uJt7tAz~Ul6C0+iGmEevsea_p|`lW6b`-s&d2!@^1m66m5XbQQ% zJD&X=RQaru+%re$^B=&~EFt8r;eUX#_CK3Q%pvzT$D{wx@PB~+JI>H6w1B7n^$I>1 zKLF0qYYXWHrD4UF86f`lvCn8So-oZu`?N@6Z_o89Gqikq`ZUFDX(wd6S=q_r^1=Iq zBJaQE9wDE38e6ksf180%f*w)kZlV;QYP)Ku65bKXE;RX$&|S3t;DHb6tD#@1<-2fR z6nra=L@y5jQQp-YD>kKSJ)kW4dNWYl&6f?(Tkea-FXfYpB(KRNs7Cg2_(aDA4mGh) zTq)9ep^5=znyYS*@6Ht$^sw|y60WswUUbIzYO_aspxeZ)S^KP(46&p$SpI(XLLUkB ziV#Lw#BeP&W=m7~M{%)?gEfYco!noH9>VXUSfu0PTsREo(98*kaOA)oH7jE}j3*k^ zoMZd@@nMk8MM?_x)PgsA6lj-qmL1Fu71R-CG@~VeGNfoKv+a zUnZ>BB!p_phDJz`bi^JdgX_b)noz~kkK)3VX@s?3hDZ54OkN4fJpOyQ?7lmi_GCNq zVe3-(dS^fi$clVYq%LUCXlrP0gA+r(++I$qs)}CiGm$JFLts7WC3RfWX!f+-PiWK; zP)R7I%7+xw;Pz1k({~Z5T*-MW<)E3cj>|aRA#ToSXOWagpE=q!oPxBEL0mOeZX|CL z-`NP%J5$eGM%>k?z7`B^+#kYjSY~WodD8kHiIuKX$Hp3nwe_7MIXh+f|7anow)Qm< zd8+e3FzR_bm)_ANb$XAKRn){&U)QCL?`$pz?%bs-cm;W2bf{{TO=OoJ|I{OX^cj~Y z=QTYk@8~ybn=&@**Nq}Qr~{L=_$8}D*~+Efu6hw>rn;3%Zh!rIafE_5VbMr5$k^h3zzl5|)$mrT zy9J7vf&iTzQ8VjKwb2A{_{hSWa!&X5XK+Je^+_1SF3Sk|U#sN}E=Md$K)Nda?D)E42R#V;T52%{U)zbD13}9>kH0 zgp>Q}4$`gH z`K0|^zzXZbH11EjX*27!&S0)yQFCRv`b?p(;`^|86(s(~=P%4-O2^RPw=ao{ho))= z))Pq#>0M$L|3mk;NrIiy?W*Y;0dv3klf*r6^CP(0w{Pq^R$eh9!d9MyQ(A}z;Z$d9 zEXHgKtx-%QXsWdb=?VF}Nhd_x&&x>dCMz%Yd4M~a4rX9>5nGw~$>jxH1n*DXn*%)N_0C-w(SGRJ; ze&!v(LLq<}hwu~nA^W_&MpTs)I-@NAD~1&G!#coRKv=g>=>Om2`hHckHs{_ z#Rm0F<^2AgqqO{?K^8ShAky!ODou2{_|^2?bLK%wd*Uc|O^kAT_0Usd=-VMtq>~k8 zWGQ`q;^LxHMF>@ zRc<^6zS3D3-ckuEe)Nvbvu0qUTD(_@<|k&~SkK~G{6I8zbMKh9ZhZ7hXgY`n_09k# zAAkyNG5ccgd}UMm9dfL-)w#eQ=H9}2kuby9lhve`f1gNCF!jK&XH0z+dlx{Beh~vM z^s<80qCa+TeSSgGvH1zQ*+hdEM4`sswogp4rMubN`@U(Xw5Zgl&XaM*KvaPFzWE z7)~+}Axo7G4I6k~E>Y@6Et_&b+KO*4vU6+d(6gJGZ?%4Q8xdG-mVe*kN~yTsJI(o= z?8GMO@iza^u!{qil-?t)Cq)4>I0oQi>n%p2i9EG6Eo4u%Y z)RA=kKtjxJ+ss;8XJ&7g!g|u~DNa@}<(QZ`V;44P(L8T_b>4aO`vyMh;p#~|Omp*A zH`UR*lR&1}z=@pslMnK+yE^?u2g6E>v58xiwl`Ex&oH7ZEOaWEI+n##1Y>SCES0V< zj+m#E#B};{zCD1Dd#m`_Dq}}2xjXw00*f8hWAy;0sD}8Kp#E0Qt3kv5RMb8*j|?oI zeidk9mJh!H_NF$JO$aiq5}STYllU}7mPykP-6riKBbAM@w<7ucgJzRqlf81NH;aiuuLK@1mC||1PLV>uJ z*E=cRpbw+3jV#h1M^^s<;skPjPN$6;Z4B6X1fUA}sI{(dDG@NFR;dxZM_5S)*rTxD zi9Chv7G_(k6S2Am1?69B?>b-5X%?D5F;$y+8%!&qo=Ex6A%B-PZyK~e{xxjWv~2YI z%-Cj_Yg_nh`g*OTy3AgP_kxD?vgoTn_4oe(J1?*Gd@r;A0N0mA^2ScM_-+!)4ZJ4N zv?JVK`rlu;-z!kGUj1BsWN3fN8XI&Yt6$A_VtcrnL$wa{nfGJfK2F?J)7}~<{WIwUgS9oLd?+L1o_NZ}tlgLVqfZySY2pg;78Vox zLt18LN-e+KUQ>=jKQiBKyYCE5m)ml$;i&80QxekfZ$I{kUxJ1zjN>YTRzj&`tAdkf zt+fgqDd=BfW2vzN^a?^NLOE9Q^Qdid1}qK?DLe2x{%!h^jOd!qBB!L^)32(dbKP$9g$htxVmZ-!OoUyY;jd0?Ir%q+**(K#AoKXFu>RDUy&jQrRr)i4j(5j zvZaEY)pAE(jPt(r-O-xwik!$SN@5OAw&y<>oW1vW9CvNO|GP{FsHwU0(&*_c#QLt9_?W~Il`80#n=cPCaR_$H_x_c!wrN@Ke7y>n_x<%m z9u{}>8J|=hn;560q71i3mU!-AIf*B6Z<)fJIVPGp%6YnHC2!P2+oikN*deSM6);KM zwU!}d_G={6LK`;Q?7jLQ;K-GDCjbVQNhk8O4@;lgQCp zO^ASe^vy*-)h4w1J4tkFThCrxw%|h>8!zrI&qRgfmgea|q~P6O$fBzjV^Ly+ zhXN=h;m6w`zc{Bp9>7L6LA#dZu7XZu^?fi1=}pjSlo>pBi!tg`m$|92g7ENX?E5`Q zIflx(#iHf*v##oQbDc&9ZlAmRaQD{2=qJ`KuS}w`0{vTNLjMlEt!nsA;e~P{w&Z95 z6YQRQhJ4RI;<)bUrJQvO^u}m#Pq{Iy=x%P&VYqOFhw&)BFv-w>h3}dBPahbT9x?P4 zE||z^uk+8Mubkm8UEG63`+t+(38Y$gD6BV!T%cUM>9ODXC*v>S;MnJu|2*uO57n?8hZcIV@pH03A9;9uuQ>DQ>NgW-0!h+n zWmK&aNLMGQLpmdx#5?kGXJ!n(M#Bsy!b~Z`4`kmjwds0K7ycU(hqSp2Sz_xFGgP1GA;6Ge6ynY9)U(7O7^|!a6P`=g7ASX9kft(xF96@# z7$lZ``nn`7jG`}QeDZLnTl|^M=Ggr4-*$HTuu9-h87B{r7ZxpfNtzHcLvokdhv-+%Sv0iSv?mC@{yXWlvbJl$J)Xm=r zt;B<@C^;uqg((h^z?C*j)TUbDM(q>e=7z%x!)JPClJ)wAzEy?m?&%|+6zi7kN-u=; zk#UF6U9BQK0}z0!RwJGWq`ENoppp|$(5t-#Gj}2W10d5GM`hpD9Yy-%{y?vkl-O7V z(@XPS3kAZvJbzyk5~%pRN7lWm!mP#bicZ!#hq(ZQ_<0O2b89%-6x7p@N8M03Qc&=v zKa}Zh7$n`53kN`4z!3jftI60}AV{UtgS+n$&$`DhHJ>~80>bK{4M3ndZ4~B?Ym;_T z-XC?DuGf0Ftu!NW66>y&=v3@@r*1&kf3vZ1-u`6k`L}szNTfxD`n4fx2A-zATv*Uk znh(pI5kEUX>;^dH+4&xY*_4*s4;X4#$<&|YTxbXW5{DXsp%WYXdtP89O-TT5?uqJ- zeMm}&?vQy&$r#l-kTKfQiiYaq-Qk zgUp}ulFa?NxP>?6$j%Te9>T3HD>uPUaHoVs7$h4FVUx>!ZK?0yJQY*KV11U&<-~qb zqtGz4`=yQYXwr#MpG+b9$=q8k_){tEn^!Cdt>EyP&#J-Mh0NDT?yY0%3FDhO!Q78@ zoxoWe8)L5G?C_{nP&QgYrhjcn7@6hA`|P%y4b|4v@2+4Mo3!0r3%YLkeV4iVYN81VJKTkTU%lb-&+GN`M)~o3t{hP!%pmIN8sjOuwvM(0 zzce(<8K=Wq-b|0xq-c53dvCZ6wn;q>d-R!Q3zUiC&fF)2mS<#VTp^>i6{_MKC5S*D zi!8TbAncNzl1=Idfuc(LPj)Arj9_h+et+34N0S-J{|BEyV83;`)?-Hr_Djrd78f0w zgDjZKbb{)|v#c`Xw#yI6{@VNOc zR(G~kd#PfQNn=Hkl@;y*W3|_?*9|k~vXqp69%s<X*IiW~a1xe`z24j~{UO zoRyR>`@Z(&tnRHDix{PhD-alMVveq)9{t62(}yqAtoKKg&*45cWk%&VY56h!D~r#U z#~ih;FBuh*+%>3{;__>DGRCOvpgqcyMRn>m&yPN9sj6Scy!APJ4i6s6xi676r?*e` ze1)%gt%bd~neD9Zvilo#jh)1|D#fOPD43L)+i~8&C~vDeG4(e~@7fiX7$$wJY`bH^XHQu|eb*xG@=vs~9F(yANz9;Gb05QU$mygGXixV`E0LyQ61p$gmS7EN@>)&ps ztb1fb&f2gdyqNBO_&asuc`KGyvH8b=l~s_9%Iu6)O=({W(y`c;(DK%D z%Oi`*-edES$#0f|S6jrJ{HK~)tV!|QdWP(K>Wy@RPs51hZKJ)JErtD+&5fk2Xaw@8+3|Q5+EjbZG(Lm2sHA2v_ncFa-eI{Vl!*Ai(UH3qb@-KseLkfoSp8??c#apzEpS*BoLQWF7mX#u2X<-05&-~FKpGJ$ z&6A#$xqnLiSrj!EHhoP%`*P@gLG?yOYUOTWs0~+Mr?=W38fn1YTkd)rKg}$-=R3~u zzF!2HA5XK8+8fI!C7#Y!w~m(;vswY9Y&x^lBEzHqH$xw{WY@1+uz&ea9JKe z`791n1`H*%Yh9zv9RkRwp{Gje$my#x?kASc;4<-Y9y-Nuu;b?1NY~6&h!CyjDDN8v z+TP=$OLbk%JJiJE9MgqZ`m=&x>D(NqeL>oD(Mi{b>45meKI z5++6+BcMGhecEQ5M8e8jXj=aOMwp;?2%a_ASwi(8NT)^=J#|&PMpnaHIW0UJ{bGXUqxiX7Bj}E>U_mtFm6U+ zK-Bnkm?U`%8_DxH&O>3!67qSo82gy7BDtPNcP|r$0h!4p8c>gBf|<4d05rGO9FsA^ z`NlV`pCaS&%7?E;j#pxb-kxUD@6aTSTODho2zT|3TR0}ldTW2ff@$U z^$(I>!II!PjE%Ezx4gn!+(N^}CADW$`3`~%$+)I7n{XdaINjb{V{h*`(+fx%)vko` zwmt}^B!)y^EJ&t|-xYdj0z;De9{Kysoy=T^X&;84BF3lO;{z|G2fG2Lf7xP-m;@dWmUt{hvm$ve~ zipv|^e=qaY0cA!q2D!O3j79o}jSwpp!ja&{?ebzyBK z-Id(eaY=6mKK|O-0=(S?)Hf~g?q+6ZECI1-0~__b{;}Yn_l`^d0Ha;$pgwh4#L`6# zdu;>rXb2xHh^w^sQ9?bstp+O}BhSY^En~`F?8Lh^CN;Q}Evb=%F@SEQB(jDjDN-w} z>}Zi;x5(sP>iMS1UPCTXB!???70i*zaUAWsRh@%YRFL&$dWJpqn{dZk${AS9FqtH7 zGnKlJE4yvA^`pEDySfms8CLvYHG`HilB_l$FL428}{_Gr|#&E&5%ged4YIc6YN z+oaihU_!?sdz{M4f1ACy$mAR&wB9=YafWxo9LQVD&5s?`Msnpp~z}878 zo>jQGk>`^6+DE#XnXV<1cf{;ktZICFolA4>8mt2bcQ_)x##m&oqQlJ13s|J`2uNf4 zND8U#-)^l{B&3I{V823*MAQA;F+#^q*bP_4L7cN8Rjdd=fS?>(REj%-Efo%PT|D`l;$?b3&_x@gE_ z?$;i76=$XCG@eC#llzY)`caBGUr>1Eqba-)Lx9g zDY8C~nUiiCYPv^L=#{JqZEqA2Czl*Z5mL3F0+g?gy##z?a^#|VgTvtGJX<${xs_h^ z>v4@|8$>fizXb(J>}bcauCvKa)oAbF@?ReAVX*_}1Sp1si{WrNJ+Wwf=hvACJC6EqfS5-}3Uq;3j8 z>$7kz{a+mk$Fwa+c~ceY^*u?z{TIUFQyY`B#NB4HwE@b^ z%%^Da+-NpQwb0?ct6M$Ri0q%^(?ejv;Bt8R1t-qPKfNm%TmK*lsq zit?}w-_~Q(Ra&*zQw*7DWfl2XhgSap(tZ7hjs1DYU1L2*&c}Ko7gI@Xbn9=p#ro@H zCux6(^&Ls>b+#!-4rZH`)6CDU2_0N~RO#-h@2-S%4{wlu>b+pyK2}rol7_mb)IVUW z)eo?$1}4d$u(yQM-Q}*iPgaZNUcvI34b|w?@##UJu%&mcOH+4HJ%D{000oIUZ?50` zyJ#dcEPZya>eH$2J^Bg(oqH;PM{lb@paNBgQb&)`rW!Ce0w`9ZwB4jIXxmt-_oW7a z0IQNnCacn(%>vY3+STr+NCT=n{gz`zsPHGlK$uD4@+5d=rt~{mRMw~5bQD|o{k~T3 zJA${uUBP3JY<0%VtH2r+j7M_P?jBsV4G?7?0qT4^>xVM4%(nG|-{IR8cs6-z?M>RH z*R5l^?;Xj!TodB?W)<@HR=IDQS#7ZMSTRd@uGpB=v}P!Y+arpAqp1d_l+ue=C3osK zY*~1D7acmKt&{54SFFCn9xz@$SAc29#&!$uTKmFDkafFq*7axjJ%d+r^ zEKFDYZFO;yt~X|QzOid&<}7vXwyWCz0LSbYLve+;;#qlbE|79N>-4#|h=wkSZVf2g zXgtq0K}z^8xaPL4n}0KBzU!ABe08N?(XDp3r+?)O;>0s5ZuuSqk;Fe&nMmZEuVHl) zc~gpbRo8dWb?M@^{Bz`de>Vrv)+@VPS9mllH;X^@mHyRPfSouYwc znJRmaXJb#5P<;9lWtyM21{{SAM7&|N3 zxS*PB-G)C5a#JK$x3*23PmA2Tk)cGz&vD+n{ zD($Cb;rEUB^UaeNmUrts*7xlk#`4}HJ4G~Gg1e|SDnqiKwdd9u^xG5mrnZHaWU#>;rB#2_wYia9%wDzX#ft{Xhql*(xy?i%`kGW#n_ zQud$crmcO28Cc@s+se-xbBy=U-Y)V z4hS<18@(xCwc>xLPjM?645>oFzzitW_aA14*-EH3*v9VEA82+OQdA*JiLE6>Ht-l^ zPy~Q6D#Un=0)b0u)gx%02;^4=o0hB%Iu&8mX$@3EXa{nD+j@{`Ubwa7*ZRPB z8Uz*9cWx{~j>LnY8Z7*KpK{(MjlRZUa?59p$Pm|d&jq~eAq%#n&yG#KJ(SXcZ9*Ad zb;$ELc(54^ftM8ors6A9h6tC;B88a48Z&f1q)vc^*seu&HLS5*TYx2n)<~pa8*3>H zr)yLi5#lt)O!AI($$4&4-#5szUptkxMUD%Vh8P)D7_yD9qA8;q)Qu#GNd@J6^OJL1 zm@hd+@9zt!BM?I`lLRay1dtoajW=~Y+NVN6&{*@HU3ER=K650|tSvv66g7@Ig#lqyUX}i?f|X%tb_Nv_kwXPiF-2XlsxLv~w?YM2L8i>-FF5s|7P`D= zp|9)&7qWpiLNDOZ)cU{M&4GF%;B7pq3P3Ys#1h|b(TdCI^S{W{{ZGsjs76?R_5k34|Rk2 zT%<(z3m6f~LH__XBOj|+mUw51;TMru@(gS-@5)-^+c9BJw8F&ho-xq%Htg^I_d!fg zA<5e*!?1jA?yo-|yS!@|57`#C-dv9zYx)yL%s(nAkOjIVDH;? z&OMsSO8fT58T#}I9?JBOl71xqT$)K6bJ5Dhk9?vS7OR3=FF-08j z1|VK%q`!%63{Wy#%+^dqt1Ckvml4?V1#}=63-3(1sf=Jp zTt^aH1&N?(WPzuSRhMuOvZ~0S8*iijp5%Qr#pM?}aRM3Qwez0d>IbwY&L>$ejB#Ue zq8TLw86H-d*edUom8k&I?DDw0fu)7@wb}T(TgsCCox8rlJ-01>k?qg}%>Z_QJ3tuU zt>5*J1pffMa$o%#?@a;ouHHsiAtIF6qJyu7l$4O9O$$#Lx@V5vokY(X2_#e`Ze>Pf z48U~?SwZ%W`chh{Yq!OEb2Oi-^qjvAyHeCm|rex|{DJDt|Kk4*~Q_8cBDgOXs zT2Ry7``c5w4{JIsf2ShoLutq}>pWyJm2h^3o;yJ~VMFz4H{MH0_zUtW_)bP>BFXY> z`^gFc8?FY=cHd(+?}1;*rkZGNTfk$FvGm&BNN0XU$Qm}2XiwaBYFeG>e|tSXnrT@k zI__V`9dipvX1>?Upf zK84groXe~>wQq+NY)c;y7-XtX)vBzSzMx4T1-y>&Ce7wVxALP*&xp3%k;VZ7{JydxK>poyh#5g4J%_WZ^%8!<=W}nE+kekTywc`F^b>mtKUtY|rIL7UpEP zl1VL<*Dx>P zPjanpAeP<|$|$oc(dqr7KG)ZzCWz-!!H?Mk$5kXcp1bC@xt@9H{{W@@Lm3mBZa~)K zC48#Q^6^>QfXu}~PypP1FVYLIJCRn}OT_iPDUAt%k?9<5}q4*|p_ zwi?icn(pD13lmn^ETjcBB)68J)5l!fuYQNL@bP$%vNvY`0F4*o*<0n_l6u9GxqPoF zmdfIJ*=^%?yNJ6U%Ni{dlkNkyrr2%O^*S*6o(;PxD!vAV+k>+roQMt?D?-$}CGpy%)Kb{E#R5@S@o)YluB z5FceDRQr8LdeWM7R=$RO$B&AorsVX01^!vM*B-f&?~%%R1;>;kNTfpq2`M#akEwD% z=nlFog?H5PvwD@9iY=?+Yc@Wg@ho;u!z*Ep5np2Ck{by`vogaRqiZ4~J1T}1trx#t zXsJ}>CRw~P1$_}Jj_8ZlX)#{+1gdX4zet;C(>i+-|^&)^C zglWj&<^&qouXZOu-~t23xYUF34xMy|*e2zTK|c{As*aj9CJ=b!EmEsMJN4=R0OO`K z7!t*u6^_yI1?o@Jq$xRNi={&=v)uyE>btQ#cUY75EQrX>1PHR-T7GK7P(Mu*VE!IF7o__{sy&tmFlcZ6jA^jBx=$f%sv{k8V-X-E|TZ>Neu#ca7VWH8JNs7_L*0;PSVvYaI6$n5eAnv9^~Y1(rpI-BdAU zGNlZRp_+_zUyj~Asld4YqN&MwtzFJriH%>m^bLQ`FxFRAb~ubKI($u>aKS9sb{5TZ za8Xo9?(D3=-0p9+(@wlvD#J;#b9%2L_jXscQ*#Q=7m(Oy_~oW9%GTQM=IFtY7wD2( ztW5-LyJSG#bBEov4`u2Pw@VHOjXKPxU#NPDKy9wa@QiR*1GF6 z9^@@$dB-8gt*m^vBW-TZXKPi*sX;3!R%(3RDnh8OJT=c=Evuz%AIG%yGkVq>w%vRc zv+1qB{6<`k`wwxCwSyIY_xJYiD@l8G65BbBK)(v8l2CT-J=@1!IO%Grv)p+ctdz@r z)uN(~i~JE2$MQJ)Smv?F8Q%*wiEa1o?5`=BQb#~47QVtYhwZHY09BhuqP<$Qmx~oCXWy^j zijOAb5^^lmxf2q>7S&68b*T}@8iYk2kTQSfgc*MUpv2*)8Qj zXitbI{3A?F2&6u2jH$2^0i!Sk)4@saprJkSox~2isYdHc4-ueR1eR5fp;xbSv~(H) zxDXc%MFFb#_-KM;A20+8pfNr@cMALTNW~SA6cHsEs2ZY#Q}bvVVI-8e+FPf)u^I%# zWR^1+k7?XGjkF+XiM7x}s0>4WBAtIOkb-%EDBIX_pSCO-#x`{Fn4Mi z+b51|IhBP*+2PoFeR}6LK4)+ojJy-o96b3I6vbtJM<*LoZ<5Z|w<DM~+-_uTcj^)r>+S_C-r@gU)Db{IHUzCw05(EVW zRb%YuK`guAxYlbs$9*Wf#pG`=_W1k<_spI@Fx2?@yTy9&v?@%Zlr^%rq!)fjlBRA^wW#_jcLo1 zDa$PHA;iGh6JRgz{(4JzZtd~vkpnhEB|lkh=41NyTKqsDV4wmL*V3+ejkv{0iBFZc zzRu_KIjFv3HG4bRq03s>Uc$)zeq!G8-DC*cm&=UM9rOTKEra!IJ85+H*nIqU6IzQ% z@YdP8OKYQeF>3-`ys=*SM%iw}jU;A9-??3)ey9K%X87+f;`TXCU6#4M#XctkaF#ho z%IvkbmdfVV>eAXsQs?51?c-=Rq-DXE>1}_^5^f7@>w*wk)?Pi zzLBn4*-I0(ExgYq6?z3xO#oVS0onk@{cisNtavB=?|>%3KMHcHxbeHpNFhE&qGc7Trx`!>EEVV4^p{b9kj^iXXBhl z2X&ITy|Ofha!&DO|tF8QOb{NL_ZrU)2Z%FJYLnD86IEPLb9+e5=OM##-9$GNoYKFc2a6qJTuemCJT&SdV`nCLzLwfk>6U| zV$u->rL43O$#ByG%%mO1XI|o+DtE2y>SJwe;Sw0%N^o#`fyy|SCAz+RXC1^%m`jX# ziaF(uKOxisGRU1{l=SKG54F=)@1YN*q9ZNEcxB%WzvmvF-sOUE`>S|zb{XWi^ETZ$ zjU5^aDI3@grl+~-(yMdm%2J#%%g(sxqZ~VsT;Q_jmdkw-#_~2l_j5vg#1aj=)eSl)B+KEh6x6357o|<7Sy+h=@ zVhokdo&%5L##bkmoGiB~YL?MV>bq4@iD^&h^aHUYs%^5Xk}EygRps1kjX}b!d8Y%t zy`KQr?}@y%%Oqm%85 zD6Ma0`6=v#lc0FR1&t*=4&KABjXLN}!Jg|iB}a^UeZ_4tT<9{DC(YO<4%ye&`8UFy4-MXu@%K2WDrtRCk z)YYl(Of+0M7YNJot}lV*SooS;25QC|i;K^+&`&x7(Z~mF$LzOb+rG1{X4IR1+5S}Q}omOO-6rHlDAzR-=pdk0Rp;w~&(#|;bt3wQs zvG=6UPt8qp+o=YNG_k=OO(_9T5?K2^nn06o5ry+_NppUrW+J-C=Ob7w*5WB_8a6S+ zXI6NW0BC4LJE+}g)%#S}HL*K6liA~Xg*f!utJWO&`2If<<;eL@7W8L@+C@64OE_Y+ zvBz5kj%$ek3s9>-HxOBxn$q^JG;!eLI)Zq4pWTs|Vk-^_7p zS)#Q@J7bC@EUH<^s~+dRxocZxwygRr*|GY58miShKOVMzEt8p#4w#ML6}H%>`INuMJP`8> z2Wv(n&b*I%%86QTXvp}7w69-r);RetVV<8e(6-Uqj(m#m^F@1(&EIi8ONY8kynag; zaU2&gwL^I_q`-TK6LlTNyK415YSn2&Jo9eFfd+-aT9d6)GH71%K(-kUz#C~)eQ0#s z(TOcdj7_l^Y;??y5$$65kJ&Uu)<>mIi`Y0Yu<9rQ6*cTV+6xl^flbGzfgwP|6;j2A zZJ-L%+n`{vC5TonN&s8@9dtmX2b6$7U-vZNBr7qHs4M^->GWuTY%+!H4NWQmJv2ar zf})^Nd_X!NTY?!%D+OQ=fgR0s;Fn=0YJ*Wh{bT;-oC(+~wE;D)C^vQ*a6_*J>cZw%B?+u=Sw{{VKJ3EV}P1}Xs{4Zzc`h$Y+<3=!(bY{Tr)X+`QqJA?FT!4Aak zD+3k$R?2SvUh@5QIT&5GG84Ogn~?*9N0 z&9@eiqsC=*$R)JbZ)(vl)mSBHSy}gn-Bc&+>Yi$=?e7vzc$c;ZnpV|qvEFq4ej=;I zM7I}FTYmAq`B_@tX0*rok*lF9M&+E+wCP^7)-Rb<+S=S5)$~l3rEKqxs>xYyvrZql zR^xo zzjdcixWO>r%{yTq|oq6%4*tdF@)`VQSjjCs}6qWN4KaqO#RrK!`j z_~^A3vf~SQAxPT#d#P>K<*n@O{$Q#W+qlwIGzNmZY1dg~|ItH+sLN;2f|@5i>=rK*(H zsrv=)IMppLz5kyNk+I05RIdSUYYAXknY2B#)8Z-0^}GjW%A>hV7`u63^x|+%2`WBSb`gA0PgRnO!cyDb|*gy_-$2aYTYcH z)%(N#6gKvDxlf#wV{fBBD2Q^6BRvfgN*!xw6j;CB)Q8 zSzaroTJDM0dOE3L_LPH7+ShW4Idz=l&vg~x{;AF>|xUCzk)IQqj;XQijIFo@%91vbdHQmG)NM?w^ zXXY?Ks>R(_YA@RMp4wLAB$c7+UO!H?vRfYNRn?{QS|)rn)JiTDJn6TvRu`Qn zlF!P4{I=T4vWZw$wGtjfXLa7Q6psyphdnzpm5>Wp*nvSP(tmW{XR`TIg*_^g>IA(TxwopOx!)^@h- zbu&h+RIyOV03;tBE9b3Tb$&&0xEV9m**ey##_KpzK9_*a6|A?i-Q0^fs*)>NvP!I_ zLk3roMHh0^s~>$de&V}bq3A#RZcnVSZlu{td$fN9-^B~sdUJ9z5M!pge=_z@`aNyp zSwygGSv>YFDuhjqQ{I$5%~i8iG`j9G$HK{K-p(qq{;8h63rDFRWrdBTzDef!cFhQUs%L_?e`1I)CUvRa@2^KdE1=P-=ony3EqVs2+nL|-x z4eWIvYq`_DTGb}|iw6?DT-A{QmDivwdj$f&Sv`ifNxp|IuT|B8magl%R`S=x z^)&iv;ygyQt~cE0w<4>iqP|_URIO#**p}(;BAy*}YL!M7&ue)G6UI6P@gD;81M+q0 z^y>67)%5|V0fK<0wd%kcgWsT_>+YfP(vT4I${H&4pdBgs&wsB@L@2k+O;@1sjRZDg=2Fk(5nyq!qfcBfAL1JH9Y?SoBmXIrzYmPo>`j9-V02v z;k;Z`l1B(q;1>-m2?^aM=J-{6_tOJqpMvol%iAoTOABX_%H!>l%GUn+6u6yXj-(CJ z1Jp6S}9I4_szbF>=lh}uhVwGjRUg>U;&Gm`p;*DmTc9YTyh({ee_FZ zWrg$Y?Q+oET70_3&_i$xFKF)|l1W-J=0w~X0w@5sYt&9b&5kzj8J}(|7Fx>k0g|x+ zZRN4fz>E@Dan_6Fe6cdA^X>tU;`1u500jR4E5+CzU!S|=+)pXVa&t+IlY?F3?WdGm z#}Y&uD6gVZ9x>AKWSVItl0DI|>eSny2&=5`tM@r6tSs(xR?}Q${!=D@b3yW$%V*Ep z+gr(xxRTY9?Oz!4ODj7}7jE`3Q)vK&yvX{+j>z6*A(uOIdnL4Z`K}H)EPo|qp2}OB zlot5A#aouP7Lov#vTu%Okz!<3F#t=)ck4Dv(9Ly}$wnmkOmVca#w2jb71loE&b!}v zkGH!?qnpWYsTw4eQy$V30BjrtT#ggR7V)nd?J>5=ar?>>ZFVJCm0e|tqll|30y?1n zod8)h0gd|I{{UFlv;;dv~c8f&EUZLe&eYiT16Dg>+0$0ZF34NiwiC}Xv^ReMZ(llr}z$aun;;5_8OMittgY7TQ;9veINBJlX{VshOMnkvE)^%QVvUr=B6`;v0JU&}G zO)b=HuMsE9B|1=d)Tkn@CDijLr*cOAq45%FL!8HL+9*f&sOS&)HGPfmR}9}dH>}op zi|$E+XdSt&o>uP@gUFQ!%dxz*LSoLr`)=|*Ok%@p8J<#*VfF354*-!)7fmxmOj%aW=?Xl(w+LCAwQUfxam= zK{UWgR{)kGqMprZNHcy$(Vq2d5yZV7<9uQmAj)zqo(kO_&R`^h2xW*3^Dm1L)f2yo zpzjK^X(aO8mr_e>9k~XO5?9$n-|N=us$@-shDwDjKF>;Mf|C?xsoFlt@fwnat-~SQ zi~}gI#X=~3wbM#^3f5Ixh}^g=Skk?~Q9vu~8fvCj$tn@;kL8N@=KV5AixSqi_YqL{ z0EA)x02^HS7tA`n4X}X1gj9PDprI(BiVgl+(8$Hy_>z21lnu7ZAE-W>;Q6(@hU&&S zGEyqV9qj;?A+?is@@iETn5vGOjZJ(rzBOaBiOs$jqtn2D!M#7j@LM~pOj@$7&E%Jc zgs1_OM;Z{c1V5kR)li7cFkn%*+FyFoTq32r@ukpxgXP@V1YrPN0+@~ zu;P4o)O=2Fkr%MXUfJB-Nq&talgoV@Ru!tpD+FJKcG8NYr963aQEpBD01Wp%H<5LU z)vRuD*ZX^;FXVopaSVlp)&Zfq%h+BixNM>?mo=5rpVYKoa`}xEXf~B*K*mQjuG(ap z>DT$5mDQA4SyszP{{Sz2(V~6lv{el`uMf&`uSd8WWr2!&jD|{4Xu^#GWFuKo821cT zFi1VM*;NpH#(OJT&2%xSivl*W2f5bgEDkQ61~&8)x<-lm{{Xx_beqs=7EDy~j5aQ4IpmFyfA3@TcKr$N|_QBovPcQ8^&AR2r037oqWsrX49{p0D;0+1eV zzX<}pNj+#Z66^<=O|@;{^eTJk4d)UYs9FJ1de{E|A*Tep1WA0d0)gXNcc;^*0^SIf znDpofBi;FQZ6(|pWJU|NAsY|0RjL?!diK**CEO!ONHwbU_Ezl^>?f?RZ*Oxq@nj?N@>V!2t2@W=mPB@8DE9AHQcvk0 zi^o}Ey`EI9)hIf7_%YLiXN~-qy0Y?=wD_-gLEaV|qeIALwa$L+v|O}{C8F8KcG5{! zqt%Ei0O`|36`ifN+jU~)TMcnxSus41>DOJ&MvK6Wy~!I~qhG0-Pu%yGcj{$~Mr4WA zVxMaERd-eCT6NaEiB+w;)tsJ^t8AGuM%v`9l3pGk1t7??;4$``zCd1XMwtbEu^V#vcV%XQa^?jOq$nI{PX{Ce%8N6qu{{a!`w+F^SZgpU@I?je!=-t8Uhaiv%a9j&A z0QsEw9e1mKpH#p7zx!-Fwc9Q?mF2HW&mFD(jlhzitd}uGIBIsOGO}(yHP&^ytG&A$ z@-SN^c-WqnY0rvZFX1F^F87YxoD#)y(q3d^xw7QNk(GAdH6v}z06tY!n1kF`SYy3w zYPr|B>T@{xa^uTaSgMCl^;W+g-qGJ3#^)SK73~iPcumTAr6N(>1c-r^}YfS--Q0*J zlgyrIcE+htTqrwOZUd!sv)wx?+oD?gOZ478O5nasE9SkTvmd2C{{V3m@C=={11^o` zPm7(gH%-uc)0`^T|cwhxQiW$nqh zw9VLD+vP1usfvF(;s_^Rofe7~zvB;D*f6DhCH_di ztt>9@CxKXmW?TD!io&3Ea=MH*Rl~d6_njW0f#&rWjek?G!)<5m50AU!u?#FRTo<~@ zMRQ}x3#%xl<5D))>z5ttTUDp8bK2%{vgOB?maI`E>C^gFzaHMv-yFu| z@iH_q-kd&K)(LUf8=_g`n%~W85}}N!Hz=vu;0QYF$xoYdv^YE-Cm}2!PcAoXd!;oh zAMxe}O7i$aK1TLcy8X+1xck!sJKJ2_U8I|WhbD{y8qf~nqo%7B-PNmT&|Ka%IQGYr z6xVXxmA_S5flDiv!Qpe+SC#IstlaV3Q!o{Z+_OB42=QU?k6n)aEc}^NsBQnu-^8HLJqX8)E+w9l^;#}zVnyT6mqHlxBIp=jH=}=?WSCw zdR(Au3M>}(5yk^FKL8UUuffu%-KoV}D4R+bF8FP^R}-zL^=@yzx_OY?T1?DyAtq`7!O=(@6uanP(>$ipSL+q_> z6TjrvIiEvJXQBtFUaHzj`?u$K+Rblxn3YK}nM7$N5Q#~Q8NM|!)uX3LcD~c_opYLu zxfiP5u<`qRbj-YJ>P((i94)+J*8UX|+|4uy<=$cq8UkdDd`VGVFeEH}NcBr8Bx#Mx z@hh0-x@%Zv7MT=Xu^7@;ebamm6w(OFAA48BNCxZbpQ;6?f=k{yx7MQM-C4to$=pbW z%GTcQRZx@uu=#gU02FmK_8kB*=6<94nT^6`Ec4g=Y8WQNT-||hV~}^2)T|LXL+8;L zJ>+TeSFK6ypai&wsJ^LLajp@H#=*y4()$oTS?-~~M~*8iDCAAe=7pHcy&~NoKG94NI6}xQ#-z#P0H@Qb)I@oCu}!kLEknJ`D|nU0QL= z`>Vo4*04j6jKTV}l}`C3*($QgJ|sU20tnE?wJAsW(C3~UdkOh2zWYe6pnvzF&p(H- zpOWkEvhJRapfub= zjzEQDa`Rm>03?n{d;HJnEKj@HO#l-s>u;>~yt?~}V))hvAtkM$$-#5xOA^awAIn$k zazeXF@C2=Eqy#Q+mwKDY^ZZqLwi^+S#@gOwmI<+U24^=T7kGsnd9qP{7whd5(*ws5 zn!^&;^j5X)+eo^OEu)%QV}b=QJgli2D5+*+v?%wIbc-yDz`3Dnwg&c|&ndMqTCk_! zPGmt-fxY^oZn{IVTGV`aS1XW!aMy*8U}H4@0CQC(Zm4;< zXEKsCf?QSWB!6KVHf_H_J{l83+`LBr0D-w!6&P!?PW6oG$W44s{1M9sQ7v6}INRU% z585gYjDu zw_@dFTjKeZRUOs&ZF+yGxwcSOGhggmh&PBu{)gBZz%yWnXGijdj=~D^87X#=AV3SI&U*fD!VCnA?RJln_iM zcR>!Xcgfq8Vn<_p+l_NRU*#Lu6GTJJ5Og%WUyw%pK=r*6d#9-KPZOoB<_|jFdpJf) zrS#okEcRxES$`m88U_XKXYDi06){#JKdwfQ!CLO-<}Ew^mR^~(BIekHHjrgTNN=c- zP>ZD%{7viw-;yMzg>9@YO%iA$JMUi4HtQ1_Xt_`LF-5o~j;gOAe3Bs#qObY%_Zp*F zf{2`uxhV&^ROn7l@qU(19loMUoM%d8)f8Jo<&VV0eJDEv+%TszWIj<3WV0gfkp2r6 zUb0k-xMUmA2-=uF4oS*FC}nn8K3BXwf6G}SFI<-u`<^%T7VB@W=69d?U}Hnz!#F>K z&sdcBcvV%j%i8bqm)Sp(6mF~Cxv`E`CLl%q#-rt~&S>s35S_s8b)hBt{r9=e*tv0S zNy^_Mi{6z#IORz;P63mu?q5)MOE`UWtM1Wa#nVY&Y(fe`Q})1W>b^{p-oY~hL*j7e z$ME|w3A-lVP-;!IC-1l-XulPm1)pGrdMIc^`S-V!rcIVbo+bA1EBt6%X=}rTs#>{* zEMDyKp;CQ1v~q^J)OH5|rXk=nfc`Q*GI{)xNQaw8%;l0nZZ|T0b@+@yK$51cXOcjV z3Onb;eD8JMwd}8jh)x6Dkyq<(o}S#A(av=1Bd;`X#IUVY4&TuWDv6N_h4J*3D6f_JJog$MQfrLP4`Kvn@^IY zz6b-^cvmN-DHtp8&Gdn<`za)8#$eX}>lIWAyP$QYycN}zP3M7O3^Oeb_KP*=@y)ed zw(PknhtzImZG<4%~w+< zWzO<&#V26A^|^YeoWrWOufgXUDCfw3q|18e<(&H^3vkmIwFiEbu1nem$t&vzi(VG^ z!PiZduGB3VCpTD0m$;R#|Kl!?IMq7G>Nm2Yt#>W$-9WoTzJa(PT%OzajC4U0k|HFv zq;`VvdT_DwW&=1Y`zP>A!?S+gnuwPKj5nhXRFB&(8rAT}{fU8~DFZAq=?X{DuxTL2 z#iE3ZQ6A1hp4E8f$v#l_i;&5`6EJxkLgsYGi2OX}H}*Z}4HT`4?ypV_@bn%l+6)d% zbPrMyco^^UXJ`k?@20EZs0ebXPHdX>&bmA zkg*iqm9R!0jS|vI_e(=ldtA@~^e_N~nAP|C{ov+(4=)}KlAK3&Gb&e@rAs{y!skUE z5J|$jp%{a|A-o;RKh>!)_VjL@Jn%or(Fn307R%=%Jl9jzswfzjb-;)iiM-*v57ZYA z2iXU!V9tZ<&4E7U`2os3Q1@C~*5vLQ_0qyYX!6f>bWxpFnA+NoiM_nY;jRqm@nK_uC&+juf9Z}Ue zdYyau&(I^FIc_XDH8EKN3ciiexDE={IO-_cl{_?(gudtZB1|{iI6HBvdfs4`4%`m9 zAe>QCre%@TX*cEg?YIJk@mVX@v3PG;V^}YkM=_Pim~sHO#o_7@V)2sp?;Pe6i(#GF zMd)C;i~f;&Bmesc+zsiNOd2=tOjwW*7aFu51b*yw+p?b2^GhQnU4jCYRa{lh0Ul;& zQFV$7aKOqjq9THF^k;FP{~%c>mh=-er7b#C9Yn04?+)zhWpZ*2X3|el6cfal-yf>{ z5zCg+-(S<*)Viqsw^{6)hN#KWXvI1xWaNezzT*+MPNTcB97p8iUNH`3Wmxfr@wE}4 zZ(vOnR=F)>A49Y6|M*7yn8Z>%pL*4dHGA{<-*w1bAj8*Yo5K3xev6WQX}_`=Mon#r zE48NIu7vW9kfpNbw-=QD&SEg~)i27j2DVV;_pp(+&u{Vs`!EC3R+df1?kV-54q%OY zK`&j0w_K93{}p4Xyw8iYQS`vh)cs&d*99_s;P}Ym zCgeORio>XuVvMaxk@8Kh$%*cOYHXHg6~S`Ho>MC8o5yyoo!dAD`lz!TN}CRyEE8|J zo2lL&$|^xC+*dx`{$NG7X9tDAC9$u zMcM1+8zB$pALCTc$Bemjf~rz3rSuQvjGzVwkODk!45nWRv*i)hExOym*YcZ5@wN6C zt?Yx@Y>4PozB^bjZ*o(%cQ+FSsrca z`TpcD#hC*3D6A!TxN|v?+E1Q-pJDKmDSPygM_0#%;xCq|N|S1>$S zC4cqbk*SIti#JEfehSC)at(4OU8WCt#JaY?+`e;B;&)&AG@0C^Tb=!X!uBC3x7h6u zo|@cA3-Qcu3rX{)knU%yC}Yt$fex*{@>kccUk?vlfhYeXJeK;_)bCPTg5OJzM4+}* zoYSj=g(2KNcLS54!1B_@pg*nSdwT*IR?PlYoo-)eWaJF6Ij~F{jM6@Vga^Q0(zWo} zJl}d77aL~sg@KoWL=tM})r>IqL`jwotmqqE)MnibrqN@g;&)ZPW+VS${cOF6r;dtX)QV#!b}>tK06%kZbXss z=bXSso&h2<^um!OL-~y&LX}Saqe#@^P-QpwN8@>+7=<}SkL2CgM~01n#Q4!u<~oOB z!ZXDC<0yp~eRp#|4$%El|t zOLmdLu^o&b!b+Tq!o-Zn3Z_Byn)Wg>7po?#TJw*{nxi{CQKt+DJE=GNU zbb89Ws+*wQ5w~du!s}k<{-Sk|uCQxR;-RY6#p6RuU85o^rb-6FyAK${H}DI0@H zGXN8jtBlv4^;*vYFz*X_ue-rRr~pE|vB?L-<|}D>zcvze-`0zwUB-x;&Yay5OpKdV z)*sZM7eMyf0o3-+AuS@gM7V?4T9|hkf%If{RMpjw$BLlx1n+ioSfd7)sQYz>;JXOc z%SuHjJ(qxE;vP?vP3$Ii!ZJKrl4`oXKVumdlVg_Y2oQe}#$cEFmaXHXNy2CESv!WG z%amN>RSc(5@3`-Ud%b+w=M5~fIMtP+afnbfZ?fA z)8d_+zsXM%zob50@jw|PZ`OK;lL>RlEv}u5pE$ympQwV932T3%kyF#er_ik z!s4dx!^|0<1zw8TmICO)BTTJc&lLbb{I0#zS|2rpnV_aOb%A*T#(ohQkF^_G8nKZ|2R1cfIO+>Of!2E@MpvK7NPHDMDl?gH5ISecpr ziGdFXV<4wqz%s42bLlei>%?H8tqrOM)2MnniX`n1tHnrb+O#eO_(+L__NQn}fssD7 zEz%n&7HH!%JxP-S6wxLcOWOdCxj5frH%wv=^FlU^j3z9a6&uL^EH)1soNl3w>mDk* z08!R7hvk);yu{PY+Y(}VMVKYsB6m7}y1sI9W^(Q@TUC?@x~)ucURlF5H}csBuUTL# zG_)TR?-?s|^RHo6t(_-&c7vkpO0D0T9XOr=u3Uzl0VeyWpc_TN;ZYwtPItA8sWwaY ziF)$Nql_|cWp^2An6GAV_IGEtzso)#_C@zkn_%$7x7yal6AM(av|vR&QUQRiQ4XNk zELx4861SL7;VXd(H6r5apm(Gx`xZ5_R@lrIpNILI(0&|aDim6it<$Wt(}tE-|B2U0 zkbD$-9wsk4($Y^uz_8AHcFfzo#q;hF1`c|7lDt>xj?o%@jSo-#w{)n8u(6MUL7h^* zB;~HCFgr)=pX&Cfg9K1@xWA{H5--c!{_Wpfzl<`g{Iv)M4@cwLhg>yP zF?ckkbtO=@DggmeL`$W^JZ-W=C+8VX) z>6uvLMzBM{l=DG56%J2ayE60C>2o_MumVGcr%K|scojpcA?Lu-ziyr)$Gl~&`xQez z`_@O~{-zxe>%;f{zQisZtUFaqqPP+U|1+Kmd zgnaSuXE5Xn&zhyMO@U2pcl^XpV^o;duNkP$wkv-B(I&>{I~6Q*;}|u_zBu*(9ZT)X zb>V#~-{ZNpCJM~kc(yh%eL@z1pa-Q`()k4H{`%1#)+>x16PHF4Z6$3mi{Pp8Y^2lf zy5_I|UsR-@!3JyV>dzR3UldQtY{Y3Ql%(BTSp`71OGv z%jj^>l4i#WgYn0$xFcv7PV2YNIDRP*sh)tnSmO>IC$S?}XSPpEk2SPiyrmbNcZzX1 zjgh1Fk@hLJSpA>uHzjmUp+0sayBnbWGXP;}6b=}6r!r5`qWe<#gA=w`6P}04c+f%6 z_~>TQhI7@f2K>G5ZSg)o%{9y!V5CK7bm`#LeHWdZ5?^d6Bs@(ot_0v@?>!Kb^y*9X zz(l9i&uv%oCH7}z! zyl9@;GDAzKT&OeOSB#(c@ZJ{0PWmUrFUnOH!(o!^E>exT)A67qRe2ITtTkcu?)QQ3 z2k3=|B*s$nR13?AI&$E6viA~RCoxNUtp zp-543u&|#*Sh+}T{kANHvJ!?rjH)l4*WjwBb#im%r6tQ^{purkRf^`3hvJZdW}4^- z^>TYOQ*;_7;1fqZRW6{@f<~KvA|=$fs9lGG<+2u6{kxeKbdKgZjL^`PHjksX-Z%ZO zqrLa7ub~;W>TJT=skZ>@|GqdDskSeczx2COx>pRRvAH_hk0Rth?+}BS?lTfDhTC`* z-$8Wd*VCjkQR9*k)hR@2YU+GBQEr%f+7c2IxVN)`w&jcMcw0yr9i|9XdL3c7T)ZVh zCzE|c%L;+$nFYc_v_;F1fmY-D^;qT#;|CQVg_wtPSdjUFgWduxGwk28TPwowI&5jo zMqX%g;)yx!{V=ahua$5YY_U9Ld9u+a`;H3zIsQ1T^~QedHiV}UV7gOYbxceBeaZ`a z%kunkY~wBd+2uteyYU<$It>{|CRJ!_#=dI=j>bHTOCH<|bKPrlu3!sI@-1 z-Tmgzu&7^a#TpRXT}7`Sa+!5C67xMFR`Dq~^_<5PBH4%!cZFO)uk$mv6cpAWKNYyb zdfXtoiXZ-Az8RL8D%OY2TLw6tU~TRV8)DO$1MBKHX!1qXXwjcnN|weVgQGo%Xc}Or z^iV*{nBRnYqSMsoy#4(|$$L~@EI2f-ZBdVBhRa3j0b})|# z8;izcbR;&9Kger!_eesY9oP?zw**<7G((LLKAB;|`=^F*MoxFCA{7dM8NW)QX??wd zf0|tX3yXUX(Pn(6^$hTZCfXM&VgHmq-U=;g>QaosuVf@YIXMFmA*O$wg$P#RNoF}S zapN=7C%;bTO#TtjJJ1(T7WoD9C!O5rwt}oQ9edKa~goZuM|nY~{pZ9G;Rl zM4Ee2abx6oHH?BPB%X(s=Z3 zedprrMDDirNLLcy8K8q`m9xBr1PbAkRFPv#bYBseLvS9$RQ`4FJ3B(RALQ;`Q$1Cjk>pr2ZWzu7gv&sJh-ie4< literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/gagged-alien.png b/r2/r2/public/static/gagged-alien.png new file mode 100644 index 0000000000000000000000000000000000000000..6f144eac02eee05e1bf17840bb52096178abc73d GIT binary patch literal 4454 zcmV-s5t;6ZP)R+CIeUNm+#6u4`D0vdFV}PT*{|RJzWu%S=f!TiVuQcy8>ini zc)TB2?RE1}>32xKL;4-k?~pox#5u5Vu03gdO|-JW>+&;JtteBk?ZVmpnS$0uV$zk@F1k`I7{u7HpV042KG&70L zMKk#C%C+{Y+RI_R{XzqM1sfaiHM66pcmCnR)mJ}0^sk-Yi*aAba=tWmxWA7#!H?}# zgGiyBNet_XalbXJF-I}lsbEYp6ST$%xGxLVe)HCrDtL&q`;5J_-{Ahe5~72gj$X`U zKNl=kQ-3*a?)Jv3KhrISj;nTlw|n7zPJ4hrBEE2D8&bwZ;bH@W8gd&nvLR+;lfBGE zOO})DMoy;DSpV8_=nTI8wyyQtx+;7Q^1CG^E+}nCSYk}@ol$+Rw6-IjprD`?D^_G= zWI&r$t5)UZNhxYZt=%{MC%Kp<$ZI zGXkFX10UJM-kUOon^Hv($CLq@!DT`~nd_EEl}Z#%?i@brEeiL2k1Us!6p78=f0kF* zT{>RZwEx>5fPg?cXn=U;%$Y;+K)Ksh09eWKv*|;_C=-d}BpJ`bY?8B>kT8;IjqNu@ zmqtZ`dEW4!M))0GuiaY%4^va4h_+x(M^E@XbXAqM{=B9zHNgs2{}^AbLqvjzq^1Wlx(?Sx=1&{1}AgQt|6uioU5s zNe~|$B-N5stWp|HNYv@GXwjmosw#?S^XAP!B6vuO2~yV}6r;59gbGv?wqQBqtV!6M zVqjbt1P0>?*wmyb+^-*`N5>58*hqt+CXS2@_R~3N^ytw5z5DgX>^}rKI8t933fX0z9!9pqu*yCxs@AzKV<4p@XM)Kf+-9I;@V2$ zS(ry$2SaoSH)XFz>^Lrtg%=a%qGkvyW?KH7%Y$x6bFTOdba7%_A|5r<`qp+GsW)^b zsC=b6mu4ZCa!)bIjX#YO^HYmDqMyaSt!+{I@LQaD1(|q+h z&iwJN8kAIEgTq5uZ{Lt!zH--9Q^WL4xTy~`(>^4Qf+?Kbn9yK6!}`O~O@eUY_aN;w zO_JVxFIoA@wdlIHFZKLb>n+}gf+XZ6P7?@>&FG69wUV9J2_DBY0R^3|z- ztUf4M3tXDq+}v^F#*G*;!r$NjSHyI{GB|MHz_xAMz%>$;lM+McOd5u=?oiFSbzhzk zb~>ENoaD%98PVNu^Cl7xW8xnvkPr_Xi3zWL4xX0#+^~Nqe{3rJ>IXrS*|TS7XJ`Lb z@hUKUHg4RQpPw&S^o0r0J-gpFf6Y<%#<0;J&rM4oYPW>>O&8F#VM=SxC(Zc`UxT{> zd*7o+kBW*4upo~fmE%sDG-=+vc|ex`Q7jJZy7}|x(-{X39z1vMT;ONe^iMziw4k5> zY$d2K$sbEpum+Hr3(_?B0IlFV&f$Z+1n#715pv-Br=EHWI|T;^Z`!m;ylQG{0{ki^f@@ zOrAU$;E7iN2FJBXwi_z$vhj;AzMxANv%90h`VI3^kRA#oSJiYlDtJODeZ+_h7A&A6 z!US{+0p8bNe_dZ+f1{9KWzZ423r0Z3qkH<~lTTtj@E+}pT|oN7`td`lG#$Qk5<=kT zk|j$-9~c7LP-den0W4QnSI2*<Lgx=_|1k052+GsZayUzFugY9RX=N{?18MAOQn- zq&Q%}de|hR>RdhFzKXy36fao7j~%=L5EWnRU7+KqOL${F(gb+VBM%F-f)@-(iJ|6M z5|CV62asNvAQ=yah9hg?LoC37C_wC%EF5y`+$LNKh$-#V& z&I@poJ+cOz0uWBf8Wbcfn=+mklQCXx><9=fz2~bk9rMz+wOj^zGXhAOQz2T)424mHl%f(@2aAOYGfj@fRI+ ze0efEu)%}R_53O}ZhFV&A}kDK@18w-KsABZIsEPGgQSC5YSjTtirAL8sjyWboa6&7G#+ZdC3 zG3p+=$mO&LJTOuZVx*;|b#A)cnZW{rJS=YPpE~I{D%%p?F+O)?cHA#44CBV^gV|}( z_MVy<-4)j=ULw?Jdghv^c*QRlKYuoBg_TON>FmL0JLw1qCVMUoTj-zpn^>RFPUnP! z1m9m(o~6#8SgnnEN+ec%BPAu9t*K{Q3s}t|<{!vLj%TS|v(2Af$c7}d;%%(?0t+3; zCOpByon7g7{``6Mf{%;Xgc1`M5e*U|?=rUDp)B|BzsBSS@ToWKo1pAYhzTawE0X8E zEN2@z(-Gu{Ql*P?fPI8v-d_O<)(3sN-d;q>?Y;dhcd{fjshk8|l;0HG$6RZ0{Q0m@~Hm*6;sfQ-$`8Z1nYj;GnNV@H60 z57=oxKfjpUZ+|Rz#<=lW+dtcpm^czNJ!}e404NFE_4LzEg9y&gUlbG^@@(Gh)YPMu22e(kl_x&jHd_l<|PaHYAWtn84@X3H3pIbuYDUw6OuwsrtnT6*xC(nAhM z*uxJ$3?o}^V>>vp$;rt_j~)dhsB-yae_wAA^pdwNoO$PbByQ9>O%V}yD)0TLV)h4z z&}GWc&+kh3s;H>w(MKQk_V!LlOiE2n?-w2pF1yPeEqInYt7Bg z1z)x6c(SsxPNW8y?;-+`mVU>+Nl4&YUSbe7L>+8b&Scb~}n}5EW5T&i(@i_6-SXZ)-bo z^2C`_-&a*t!in0lWea#j?gw?slqneBDx8-(At9X0OFHHH7#}DkjiejVL=1yg?{70W z5aTMdX3Y}Q#}3g>6pUq)pmhAZdCTqF%ID^wz=G*36edKCA`clMUoUPZqH z=7TeXD~9PGdE^nm2FK(6`|k()-+jN`#TT49fVX$=-oU`X!-o$G6B|Z*`|Y&opuX--7<2t zh6^n#Kd<`a%kM0=V?eb2{rg8mL?k68;bbCo4yy<81%o#=HC0qpl$Dj$)YPCT69tU7 zzdkTDdt_wy+q~6VQ(|m$X^vV%ngQv0;WU%e6g+~amgG8`Q>owW9jPlh-ne#ejmMk6 zE|#>|z?@N$nRnR1r=}^Rmb4j9GMU`ZbCpU|5@?#{o{cCI_N*6JhMH@*&{|o05$`t7 zH??8n#xIqegpgkTx9PWF sk$&&WaS{oOnwp$NGO=#H@?QZ40OJJLiwKxBng9R*07*qoM6N<$f=sr4xBvhE literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/gradient-button-hover.png b/r2/r2/public/static/gradient-button-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..6b93d8a65c958e46373ae4b1f5243ae407c25c03 GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^j6f{U!2~2%t~M|QQj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS>Jih?~|978H@<(%Be+u*>%eBea2fqt|BYx0J};ywmdKI;Vst0PvtZlK=n! literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/gradient-button.png b/r2/r2/public/static/gradient-button.png new file mode 100644 index 0000000000000000000000000000000000000000..4274d8c5ea6bea8449131a5e2eebe4bf0e7d67e7 GIT binary patch literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^j6f{Q!2~3qBu}&hQj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS>Jip)J-978H@mGo>BJfOgHB;~z*Ap>`xNa19s??ImfrB?S; vN}OhSG;^xcD{oWr+4bJpJM8Oo7}*$%eyV?a>`-_JXas|&tDnm{r-UW|7N9U7 literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/gradient-nub-hover.png b/r2/r2/public/static/gradient-nub-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..c446faa083d98d9551296cb5db6a2956b8ea5faf GIT binary patch literal 773 zcmV+g1N!`lP);j)4oUAR;1s#GXg{GhmSC5j7mA^2IjsED8A>gWvK)GeLK6O z9pj-;<9qNMKnwY})aA|1)E(`l9oq(}tFvP$7!2yTejHfhi-C}m=Ft`HpzVVL)ZVss z4EOTD9zgNUz(s%sXnl7V^>6NLSlQZg9Wk@O*4hmOgezcKBtn~dH&XA0p7uzj=?P*V z15vLA%GA4WvYNmzUt^@jH7b?MS3nP;o93!An0%X(Uf$@_>?|d;1UW(+1ic6&zM#Rw zXQ69Wb%32GU%jGDUOj0J3are+}n7sMR;NDA3F1FWQ z6J6Kc%qBqh!6^O9;O>jK^lR6~W3eAb4(BI8kHSbOcb>(hpQC|ujwXmxO4nyVw=j2e z`8s<70a~-JjTA-c(UZwDObUMu=2uJWDuTffE8CpvwNzDI!RAVKUBrcO0Fh?4BkE#M z%%t4I1;*>3Z)9~U7qd@sI?f!&zaraafh;3Oj#WAjdf1&)%=V#+gk2_X=E*Vh)1U_+ z{Am6j00000NkvXXu0mjf DyRc`E literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/gradient-nub.png b/r2/r2/public/static/gradient-nub.png new file mode 100644 index 0000000000000000000000000000000000000000..b271adab07c7fc5f6392c1d07336cdb695974741 GIT binary patch literal 732 zcmV<20wev2P)P0018d1^@s660l}|0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!en~_@RCwC7mcNS=K@`W|X7?^ROb+yj z#i55tD!XfJ{39%{u+YX%@u~wsQ2YU6VI_)%SO_Op7T4Mw{=!BC(Z={2QIlMf{gutm z><_}DquzS(`Vv+u*a_hxpqXCKTbMD!GT0DYnf?O1J*vNo;Lld0T3spgb$JTobbdr&FqRNKEmhpP(*%SHVax?VzurV5up zywVIr_^4q?@`LC;%(Doan;PP$gbdpEwx7U|O0L4YfCUpFDBC#e^gS6-)56Sb>A*Yz zZ2>mckLHQ&kU_J&AX#5sFZc-aI6&`4LfjmXL1R$M7QC3ZV4ec3y$j;_`62&{qw7=4 zkrYPpRhSn6ixP<4R|iQJGu-_)6IdPQDnL#lCbd5uD{mRwRJAf+1LVfY6|YLw${hP& z<3A%-8mytMMAKONp4Y9iF(U>ipBHSKb277;G%Vi4>CWG9%>cmF)pq4;++Io`q>y4_ z2f&w>JQ3c8d2X<;DVYgYI(6Y3dJpq7oWB@;o+~GuT!aM@zhGW~^LwMl88l_Q*m=_y z9&T?!*SU(ujWx15@Ad-hy7+j>XcOjnIR7^3ozxIlgh||;ez*;D9nRmReRm>6gh;BJ zR1I%<8*u(O>+HuuxFB|1K|78#VP1xAF>{*yjf8ZN)P|j$yV&tH&U2m;-vzPzL6X>> z2stDhP+i3$HE>!XiBltguL0p?u0z`@w;AJ0%X^FKXK3@^kyLs6BftQfK#~oSvX!j> O0000)[^>]*$|^#([\w-]+)$/, + // Is it a simple selector + isSimple = /^.[^:#\[\.,]*$/; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + // Make sure that a selection was provided + selector = selector || document; + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this[0] = selector; + this.length = 1; + this.context = selector; + return this; + } + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + var match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) + selector = jQuery.clean( [ match[1] ], context ); + + // HANDLE: $("#id") + else { + var elem = document.getElementById( match[3] ); + + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem && elem.id != match[3] ) + return jQuery().find( selector ); + + // Otherwise, we inject the element directly into the jQuery object + var ret = jQuery( elem || [] ); + ret.context = document; + ret.selector = selector; + return ret; + } + + // HANDLE: $(expr, [context]) + // (which is just equivalent to: $(content).find(expr) + } else + return jQuery( context ).find( selector ); + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) + return jQuery( document ).ready( selector ); + + // Make sure that old selector state is passed along + if ( selector.selector && selector.context ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return this.setArray(jQuery.makeArray(selector)); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.3.1", + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num === undefined ? + + // Return a 'clean' array + jQuery.makeArray( this ) : + + // Return just the object + this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = jQuery( elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) + ret.selector = this.selector + (this.selector ? " " : "") + selector; + else if ( name ) + ret.selector = this.selector + "." + name + "(" + selector + ")"; + + // Return the newly-formed element set + return ret; + }, + + // Force the current matched set of elements to become + // the specified array of elements (destroying the stack in the process) + // You should use pushStack() in order to do this, but maintain the stack + setArray: function( elems ) { + // Resetting the length to 0, then using the native Array push + // is a super-fast way to populate an object with array-like properties + this.length = 0; + Array.prototype.push.apply( this, elems ); + + return this; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem && elem.jquery ? elem[0] : elem + , this ); + }, + + attr: function( name, value, type ) { + var options = name; + + // Look for the case where we're accessing a style value + if ( typeof name === "string" ) + if ( value === undefined ) + return this[0] && jQuery[ type || "attr" ]( this[0], name ); + + else { + options = {}; + options[ name ] = value; + } + + // Check to see if we're setting style values + return this.each(function(i){ + // Set all the styles + for ( name in options ) + jQuery.attr( + type ? + this.style : + this, + name, jQuery.prop( this, options[ name ], type, i, name ) + ); + }); + }, + + css: function( key, value ) { + // ignore negative width and height values + if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) + value = undefined; + return this.attr( key, value, "curCSS" ); + }, + + text: function( text ) { + if ( typeof text !== "object" && text != null ) + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + + var ret = ""; + + jQuery.each( text || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + ret += this.nodeType != 1 ? + this.nodeValue : + jQuery.fn.text( [ this ] ); + }); + }); + + return ret; + }, + + wrapAll: function( html ) { + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).clone(); + + if ( this[0].parentNode ) + wrap.insertBefore( this[0] ); + + wrap.map(function(){ + var elem = this; + + while ( elem.firstChild ) + elem = elem.firstChild; + + return elem; + }).append(this); + } + + return this; + }, + + wrapInner: function( html ) { + return this.each(function(){ + jQuery( this ).contents().wrapAll( html ); + }); + }, + + wrap: function( html ) { + return this.each(function(){ + jQuery( this ).wrapAll( html ); + }); + }, + + append: function() { + return this.domManip(arguments, true, function(elem){ + if (this.nodeType == 1) + this.appendChild( elem ); + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function(elem){ + if (this.nodeType == 1) + this.insertBefore( elem, this.firstChild ); + }); + }, + + before: function() { + return this.domManip(arguments, false, function(elem){ + this.parentNode.insertBefore( elem, this ); + }); + }, + + after: function() { + return this.domManip(arguments, false, function(elem){ + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + }, + + end: function() { + return this.prevObject || jQuery( [] ); + }, + + // For internal use only. + // Behaves like an Array's .push method, not like a jQuery method. + push: [].push, + + find: function( selector ) { + if ( this.length === 1 && !/,/.test(selector) ) { + var ret = this.pushStack( [], "find", selector ); + ret.length = 0; + jQuery.find( selector, this[0], ret ); + return ret; + } else { + var elems = jQuery.map(this, function(elem){ + return jQuery.find( selector, elem ); + }); + + return this.pushStack( /[^+>] [^+>]/.test( selector ) ? + jQuery.unique( elems ) : + elems, "find", selector ); + } + }, + + clone: function( events ) { + // Do the clone + var ret = this.map(function(){ + if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // In order to get around this, we use innerHTML. + // Unfortunately, this means some modifications to + // attributes in IE that are actually only stored + // as properties will not be copied (such as the + // the name attribute on an input). + var clone = this.cloneNode(true), + container = document.createElement("div"); + container.appendChild(clone); + return jQuery.clean([container.innerHTML])[0]; + } else + return this.cloneNode(true); + }); + + // Need to set the expando to null on the cloned set if it exists + // removeData doesn't work here, IE removes it from the original as well + // this is primarily for IE but the data expando shouldn't be copied over in any browser + var clone = ret.find("*").andSelf().each(function(){ + if ( this[ expando ] !== undefined ) + this[ expando ] = null; + }); + + // Copy the events from the original to the clone + if ( events === true ) + this.find("*").andSelf().each(function(i){ + if (this.nodeType == 3) + return; + var events = jQuery.data( this, "events" ); + + for ( var type in events ) + for ( var handler in events[ type ] ) + jQuery.event.add( clone[ i ], type, events[ type ][ handler ], events[ type ][ handler ].data ); + }); + + // Return the cloned set + return ret; + }, + + filter: function( selector ) { + return this.pushStack( + jQuery.isFunction( selector ) && + jQuery.grep(this, function(elem, i){ + return selector.call( elem, i ); + }) || + + jQuery.multiFilter( selector, jQuery.grep(this, function(elem){ + return elem.nodeType === 1; + }) ), "filter", selector ); + }, + + closest: function( selector ) { + var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null; + + return this.map(function(){ + var cur = this; + while ( cur && cur.ownerDocument ) { + if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) + return cur; + cur = cur.parentNode; + } + }); + }, + + not: function( selector ) { + if ( typeof selector === "string" ) + // test special case where just one selector is passed in + if ( isSimple.test( selector ) ) + return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector ); + else + selector = jQuery.multiFilter( selector, this ); + + var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; + return this.filter(function() { + return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; + }); + }, + + add: function( selector ) { + return this.pushStack( jQuery.unique( jQuery.merge( + this.get(), + typeof selector === "string" ? + jQuery( selector ) : + jQuery.makeArray( selector ) + ))); + }, + + is: function( selector ) { + return !!selector && jQuery.multiFilter( selector, this ).length > 0; + }, + + hasClass: function( selector ) { + return !!selector && this.is( "." + selector ); + }, + + val: function( value ) { + if ( value === undefined ) { + var elem = this[0]; + + if ( elem ) { + if( jQuery.nodeName( elem, 'option' ) ) + return (elem.attributes.value || {}).specified ? elem.value : elem.text; + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type == "select-one"; + + // Nothing was selected + if ( index < 0 ) + return null; + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + if ( option.selected ) { + // Get the specifc value for the option + value = jQuery(option).val(); + + // We don't need an array for one selects + if ( one ) + return value; + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + } + + // Everything else, we just grab the value + return (elem.value || "").replace(/\r/g, ""); + + } + + return undefined; + } + + if ( typeof value === "number" ) + value += ''; + + return this.each(function(){ + if ( this.nodeType != 1 ) + return; + + if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) ) + this.checked = (jQuery.inArray(this.value, value) >= 0 || + jQuery.inArray(this.name, value) >= 0); + + else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(value); + + jQuery( "option", this ).each(function(){ + this.selected = (jQuery.inArray( this.value, values ) >= 0 || + jQuery.inArray( this.text, values ) >= 0); + }); + + if ( !values.length ) + this.selectedIndex = -1; + + } else + this.value = value; + }); + }, + + html: function( value ) { + return value === undefined ? + (this[0] ? + this[0].innerHTML : + null) : + this.empty().append( value ); + }, + + replaceWith: function( value ) { + return this.after( value ).remove(); + }, + + eq: function( i ) { + return this.slice( i, +i + 1 ); + }, + + slice: function() { + return this.pushStack( Array.prototype.slice.apply( this, arguments ), + "slice", Array.prototype.slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function(elem, i){ + return callback.call( elem, i, elem ); + })); + }, + + andSelf: function() { + return this.add( this.prevObject ); + }, + + domManip: function( args, table, callback ) { + if ( this[0] ) { + var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), + scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ), + first = fragment.firstChild, + extra = this.length > 1 ? fragment.cloneNode(true) : fragment; + + if ( first ) + for ( var i = 0, l = this.length; i < l; i++ ) + callback.call( root(this[i], first), i > 0 ? extra.cloneNode(true) : fragment ); + + if ( scripts ) + jQuery.each( scripts, evalScript ); + } + + return this; + + function root( elem, cur ) { + return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ? + (elem.getElementsByTagName("tbody")[0] || + elem.appendChild(elem.ownerDocument.createElement("tbody"))) : + elem; + } + } +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +function evalScript( i, elem ) { + if ( elem.src ) + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + + else + jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + + if ( elem.parentNode ) + elem.parentNode.removeChild( elem ); +} + +function now(){ + return +new Date; +} + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) + target = {}; + + // extend jQuery itself if only one argument is passed + if ( length == i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) + // Extend the base object + for ( var name in options ) { + var src = target[ name ], copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) + continue; + + // Recurse if we're merging object values + if ( deep && copy && typeof copy === "object" && !copy.nodeType ) + target[ name ] = jQuery.extend( deep, + // Never move original objects, clone them + src || ( copy.length != null ? [ ] : { } ) + , copy ); + + // Don't bring in undefined values + else if ( copy !== undefined ) + target[ name ] = copy; + + } + + // Return the modified object + return target; +}; + +// exclude the following css properties to add px +var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, + // cache defaultView + defaultView = document.defaultView || {}, + toString = Object.prototype.toString; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) + window.jQuery = _jQuery; + + return jQuery; + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return toString.call(obj) === "[object Function]"; + }, + + isArray: function( obj ) { + return toString.call(obj) === "[object Array]"; + }, + + // check if an element is in a (or is an) XML document + isXMLDoc: function( elem ) { + return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || + !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument ); + }, + + // Evalulates a script in a global context + globalEval: function( data ) { + data = jQuery.trim( data ); + + if ( data ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + if ( jQuery.support.scriptEval ) + script.appendChild( document.createTextNode( data ) ); + else + script.text = data; + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, length = object.length; + + if ( args ) { + if ( length === undefined ) { + for ( name in object ) + if ( callback.apply( object[ name ], args ) === false ) + break; + } else + for ( ; i < length; ) + if ( callback.apply( object[ i++ ], args ) === false ) + break; + + // A special, fast, case for the most common use of each + } else { + if ( length === undefined ) { + for ( name in object ) + if ( callback.call( object[ name ], name, object[ name ] ) === false ) + break; + } else + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} + } + + return object; + }, + + prop: function( elem, value, type, i, name ) { + // Handle executable functions + if ( jQuery.isFunction( value ) ) + value = value.call( elem, i ); + + // Handle passing in a number to a CSS property + return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, classNames ) { + jQuery.each((classNames || "").split(/\s+/), function(i, className){ + if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) + elem.className += (elem.className ? " " : "") + className; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, classNames ) { + if (elem.nodeType == 1) + elem.className = classNames !== undefined ? + jQuery.grep(elem.className.split(/\s+/), function(className){ + return !jQuery.className.has( classNames, className ); + }).join(" ") : + ""; + }, + + // internal only, use hasClass("class") + has: function( elem, className ) { + return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( var name in options ) + elem.style[ name ] = old[ name ]; + }, + + css: function( elem, name, force ) { + if ( name == "width" || name == "height" ) { + var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; + + function getWH() { + val = name == "width" ? elem.offsetWidth : elem.offsetHeight; + var padding = 0, border = 0; + jQuery.each( which, function() { + padding += parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; + border += parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; + }); + val -= Math.round(padding + border); + } + + if ( jQuery(elem).is(":visible") ) + getWH(); + else + jQuery.swap( elem, props, getWH ); + + return Math.max(0, val); + } + + return jQuery.curCSS( elem, name, force ); + }, + + curCSS: function( elem, name, force ) { + var ret, style = elem.style; + + // We need to handle opacity special in IE + if ( name == "opacity" && !jQuery.support.opacity ) { + ret = jQuery.attr( style, "opacity" ); + + return ret == "" ? + "1" : + ret; + } + + // Make sure we're using the right name for getting the float value + if ( name.match( /float/i ) ) + name = styleFloat; + + if ( !force && style && style[ name ] ) + ret = style[ name ]; + + else if ( defaultView.getComputedStyle ) { + + // Only "float" is needed here + if ( name.match( /float/i ) ) + name = "float"; + + name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); + + var computedStyle = defaultView.getComputedStyle( elem, null ); + + if ( computedStyle ) + ret = computedStyle.getPropertyValue( name ); + + // We should always get a number back from opacity + if ( name == "opacity" && ret == "" ) + ret = "1"; + + } else if ( elem.currentStyle ) { + var camelCase = name.replace(/\-(\w)/g, function(all, letter){ + return letter.toUpperCase(); + }); + + ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { + // Remember the original values + var left = style.left, rsLeft = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + style.left = ret || 0; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + elem.runtimeStyle.left = rsLeft; + } + } + + return ret; + }, + + clean: function( elems, context, fragment ) { + context = context || document; + + // !context.createElement fails in IE with an error but returns typeof 'object' + if ( typeof context.createElement === "undefined" ) + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) { + var match = /^<(\w+)\s*\/?>$/.exec(elems[0]); + if ( match ) + return [ context.createElement( match[1] ) ]; + } + + var ret = [], scripts = [], div = context.createElement("div"); + + jQuery.each(elems, function(i, elem){ + if ( typeof elem === "number" ) + elem += ''; + + if ( !elem ) + return; + + // Convert html string into DOM nodes + if ( typeof elem === "string" ) { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ + return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? + all : + front + ">"; + }); + + // Trim whitespace, otherwise indexOf won't work as expected + var tags = jQuery.trim( elem ).toLowerCase(); + + var wrap = + // option or optgroup + !tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [ 1, "", "
" ] || + + !tags.indexOf("", "" ] || + + // matched above + (!tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + // IE can't serialize and + + + + + +
+
+
+
+ +
+
+
+ + + + +
+
+
+ +
+
+
+ +
+
+
+
+
Track your karma in real-time
+
You deserve better than just mashing reload to see how your reddit-worth fluctuates. Now it’s always on your desktop.
+
+
+
+
+ +
+ +
+ +
+
+
+
    +
  • Supports Windows, OSX, and Linux
  • +
  • New message alerts for reddit mail
  • +
  • Color it to your tastes, or leave it semi-transparent
  • +
  • Sound notifications with multiple sound packs
  • +
  • Minimizes to your tray or dock
  • +
+
+
+
+ +
+
+ +
+
+
+
+
Karma graphs the whole world can enjoy
+
Track it like your stock portfolio (hopefully with better results)
Pretty charts are a great addition to any resume.
+
+
+
+
+ +
+ +
+ +
+
+
+
    +
  • Shows up to 10 hours of your karma history
  • +
  • Export the graph as a PNG to preserve the moment
  • +
  • Customize the line colors to suit your style
  • +
  • Created by a redditor, for redditors
  • +
  • Be the envy of all your reddit friends
  • +
+
+
+
+ +
+
+
+
+
+ +
+ + Want to get in on the reddit development scene like tritelife?
+ Click here to dive in. +
+
+ + + + + diff --git a/r2/r2/public/static/socialite/appsbar/barbg.png b/r2/r2/public/static/socialite/appsbar/barbg.png new file mode 100755 index 0000000000000000000000000000000000000000..f65ccc7ae75e536f8c808e1f1cb9a594ac4093c9 GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^j6kf;!2~27o3?%hQj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS>Jikv-N978H@#dJGzF&OeNUHxxgdb8Mnqo(O}rz3_da!$AP zR1}5o`8-iOX8Y$)Is5B58$7lh(`=Rgwa#MA%Q^mL55y7}PPCfI-1;$(6KEWRr>mdK II;Vst03oV4g#Z8m literal 0 HcmV?d00001 diff --git a/r2/r2/public/static/socialite/appsbar/buttonbg.png b/r2/r2/public/static/socialite/appsbar/buttonbg.png new file mode 100755 index 0000000000000000000000000000000000000000..f6792c4c79b5f99defe5bbf86f7be57cf84997e2 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^j6iJ1!2~2zqn|AUQj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS>Jie`GcIEGZ*syV+~sL4R2<)P~P|8Zerea$_K1m2xI+1tgU zb@*kD&c3|#f00LKJh;P^Y|~*gr7U8~;XM&Pn~%=mo$P2Yb>iTHx17H2KRlf7X2q-v z5^+^L;yH&U?`co<(hn7xHuu>|lRw{E{FW(Y|03zI5YwmY4j0PQzx{nK$V=zgiEX*f vL9g1c%@Pd!mpo(ZH0yiSVonydtP%{LtrxgXI?s3&=tu@nS3j3^P6DSr z1<%~X^wgl##FWaylc_d9MMa)2jv*DdlK%YvZ_jMD;q?XCG=-_mM}qIX3Hg^4e7ME^ zF_)*|Jx&FwQ|fCZJ(D{o%@JVkIF`yFY}c#MaGpV}Mk - - - - - -
-
-
-
- - -
-Install it for free! -

If you like it, why not thank chromakode, the redditor who made it, with some cash so he can buy some bacon. -

-
- -
- -
-
-
- - - -
-
- - - -
75% goes to chromakode
-24% pays for servers
-1% pays for this
- -
-
-
- -
-

What does it do?

-
    -
  • reddit functionally unobtrusively integrated into Firefox!
  • -
  • magically appears when you click a reddit link to let you vote, save, and hide links right there!
  • -
  • unlock the digital fairy dust that is the serendipity button!
  • -
  • achieve the notoriety you feel has long been wasted on your inferior peers!
  • -
  • enhance your reddit experience so much that you start using exclamation points!
  • -
-
- -

-

Socialite screenshot
-

- -

How to use it

- -

- Although Socialite appears auto-magically, you can also open the bar manually by clicking on the reddit - icon on the right side of your location bar: -

- -

-

lookup-or-submit button
-

- -

- If the page is not submitted to reddit, or you click again, a submit - bar will appear: -

- -

-

submit bar screenshot
-

- -

- (PRO-TIP: if you want to skip straight to the submit bar, - middle-click on the reddit icon.) -

- - -

Configuration

- -

- Like a Mr. Potato Head, you can change its appearance -- choose what buttons are displayed in the toolbar in the - extensions preferences: -

- -

-

site properties window screenshot
-

- -

Activate for other reddit sites

-

- Our toolbar gets around. Since reddit lets you - - host reddits from other domains - like - the Cute List - Socialite will also let you edit the list of domains that it works for. -

-

-

preferences window screenshot
-

- -
- -
Want to get in on the reddit development scene like chromakode?
Click here to dive in.
- -
- - + + + + Socialite + + + + + +
+
+ +

Install it for free!

+
+ If you like it, why not thank chromakode, the redditor who made it, with some cash so he can buy some bacon. +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ 75% goes to chromakode
+ 24% pays for servers
+ 1% pays for this +
+
+
+ +
+
+

What does it do?

+
    +
  • reddit functionally unobtrusively integrated into Firefox!
  • +
  • magically appears when you click a reddit link to let you vote, save, and hide links right there!
  • +
  • unlock the digital fairy dust that is the serendipity button!
  • +
  • achieve the notoriety you feel has long been wasted on your inferior peers!
  • +
  • enhance your reddit experience so much that you start using exclamation points!
  • +
+
+ +
Socialite screenshot
+

How to use it

+ Although Socialite appears auto-magically, you can also open the bar manually by clicking on the reddit icon on the right side of your location bar: + +
lookup-or-submit button
+ If the page is not submitted to reddit, or you click again, a submit bar will appear: + +
submit bar screenshot
+ (PRO-TIP: if you want to skip straight to the submit bar, middle-click on the reddit icon.) + + +

Configuration

+ Like a Mr. Potato Head, you can change its appearance -- choose what buttons are displayed in the toolbar in the extensions preferences: + +
site properties window screenshot
+ +

Activate for other reddit sites

+ Our toolbar gets around. Since reddit lets you + host reddits from other domains - like + the Cute List - Socialite will also let you edit the list of domains that it works for. + +
preferences window screenshot
+ + + +
+
+ +
Want to get in on the reddit development scene like chromakode?
Click here to dive in.
+
+
+
+ + diff --git a/r2/r2/public/static/socialite/socialitelogo.png b/r2/r2/public/static/socialite/socialitelogo.png old mode 100644 new mode 100755 index 2e149b0fa3161c97dc0a2573d2cd172adb2002f0..daf73c6980c0471dbdd1e34b43b6992c4a673bae GIT binary patch literal 19793 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 r>6GQ)%3y5ArAp + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

${thing.award.title}

+
+ recipient + + + ${error_field("NO_USER", "recipient", "span")} + ${error_field("USER_DOESNT_EXIST", "recipient", "span")} +
+ description / period + + +
+ url + + +
+ hours to show cup + + + ${error_field("BAD_NUMBER", "cup_hours", "span")} +
+ + + + + +

+ back to awards +

+
+ diff --git a/r2/r2/templates/adminawards.html b/r2/r2/templates/adminawards.html new file mode 100644 index 000000000..8edd74e9b --- /dev/null +++ b/r2/r2/templates/adminawards.html @@ -0,0 +1,96 @@ +## The contents of this file are subject to the Common Public Attribution +## License Version 1.0. (the "License"); you may not use this file except in +## compliance with the License. You may obtain a copy of the License at +## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +## License Version 1.1, but Sections 14 and 15 have been added to cover use of +## software over a computer network and provide for limited attribution for the +## Original Developer. In addition, Exhibit A has been modified to be consistent +## with Exhibit B. +## +## Software distributed under the License is distributed on an "AS IS" basis, +## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +## the specific language governing rights and limitations under the License. +## +## The Original Code is Reddit. +## +## The Original Developer is the Initial Developer. The Initial Developer of +## the Original Code is CondeNet, Inc. +## +## All portions of the code written by CondeNet are Copyright (c) 2006-2009 +## CondeNet, Inc. All Rights Reserved. +################################################################################ + +<%namespace file="utils.html" import="error_field"/> + +<%def name="awardbuttons(codename)"> + + + +<%def name="awardedit(fullname, title='', codename='', imgurl='')"> + + + + + + + + + + + + + %for award in thing.awards: + + + + + + + + %endfor + +
fncnimgtitlebuttons
${award._fullname}${award.codename}${award.title} + ${awardbuttons(award.codename)} + ${awardedit(award._fullname, award.title, award.codename, award.imgurl)} +
+ + + +${awardedit("NEW")} diff --git a/r2/r2/templates/userstats.html b/r2/r2/templates/adminawardwinners.html similarity index 51% rename from r2/r2/templates/userstats.html rename to r2/r2/templates/adminawardwinners.html index 22d36da5a..348b97a82 100644 --- a/r2/r2/templates/userstats.html +++ b/r2/r2/templates/adminawardwinners.html @@ -20,45 +20,46 @@ ## CondeNet, Inc. All Rights Reserved. ################################################################################ -<%namespace file="utils.html" import="plain_link"/> - -##

awards

-## -## -###for a in $awards -## #set u = $user_objs[a["uid"]] -## -###end for -##
today
$a['award']${plain_link(u.name, "/user/%s" % u.name)} ($sanekarma($u.pop))
-###end if - -

${_("biggest karma gainers")}

- - - - %for user, change in thing.top_day: +<%def name="winnerline(trophy)"> - - + + + + - %endfor -
${_("today")}
${plain_link(user.name, "/user/%s" % user.name)} (${user.link_karma})+${change} + ${trophy._thing1.name} + + ${trophy._name} + + ${getattr(trophy, "description", "")} + + %if hasattr(trophy, "url"): + ${trophy.url} + %endif +
+ - - - %for user, change in thing.top_week: +
${_("this week")}
- - + + + + + %for trophy in thing.trophies: + ${winnerline(trophy)} %endfor
${plain_link(user.name, "/user/%s" % user.name)} (${user.link_karma})+${change} + + +

${thing.award.title}

+
+ description + + url +
- - - %for user in thing.top_users: - - - - %endfor -
${_("all-time")}
${plain_link(user.name, "/user/%s" % user.name)} (${user.link_karma})
+

+ back to awards +

+ diff --git a/r2/r2/templates/admintranslations.html b/r2/r2/templates/admintranslations.html index 32e43b3e9..8ab0899d4 100644 --- a/r2/r2/templates/admintranslations.html +++ b/r2/r2/templates/admintranslations.html @@ -40,7 +40,7 @@

- +
%for t in thing.translations: <% name = Translator.get_name(t) diff --git a/r2/r2/templates/ads.html b/r2/r2/templates/ads.html index 8cfe770da..31bc4629e 100644 --- a/r2/r2/templates/ads.html +++ b/r2/r2/templates/ads.html @@ -21,13 +21,33 @@ ################################################################################ <%! from r2.lib.template_helpers import get_domain, static + from r2.lib.tracking import AdframeInfo import random %> - -## - +%if c.site.ad_type == "custom" or c.site.ad_file != c.site._defaults.get("ad_file"): + +%elif c.site.ad_type == "basic": + <% name = c.site.name if not c.default_sr else '' %> + +%else: + +%endif + diff --git a/r2/r2/templates/appservicemonitor.html b/r2/r2/templates/appservicemonitor.html index cb04d84fa..a9a68206f 100644 --- a/r2/r2/templates/appservicemonitor.html +++ b/r2/r2/templates/appservicemonitor.html @@ -30,8 +30,10 @@ # default number of cpus shall be 1 ncpus = getattr(host, "ncpu", 1) # color code in nlevel levels - return min(max(int(nlevels*host.load()/ncpus+0.4), 0),nlevels+1) - + return _load_int(host.load(), ncpus, nlevels = 8) + def _load_int(current, max_val, nlevels = 8): + return min(max(int(nlevels*current/max_val+0.4), 0),nlevels+1) + %>
@@ -51,6 +53,32 @@ %endfor
+ %if any(getattr(h, "queue", None) for h in thing.hostlogs): + + + + + + %for host in thing.hostlogs: + %if host.queue: + + + + %for name, data in host.queue: + <% + length = data() + max_len = host.queue.max_length(name) + load_level = _load_int(length, max_len) + %> + + + + + %endfor + %endif + %endfor +
queuelength
${name}${length} / ${max_len}
+ %endif %if any(h.database for h in thing.hostlogs): @@ -239,7 +267,8 @@ %endfor

- + + + + + + -<%call expr="image_upload('/api/upload_sr_img', thing.site.header, - tabular=True, - label = _('upload header image'))"> - - - - - - - - - + %if c.user_is_admin: + <% + ad_type = "default" + if thing.site.ad_type == "custom" or thing.site.ad_file != thing.site._defaults.get("ad_file"): + ad_type = "custom" + elif thing.site.ad_type == "basic": + ad_type = "basic" + %> + <%utils:line_field title="${_('ad frame')}"> +
+ + + + - -%else: -
+ + + + ${_("Use the same ad frame as on the front page.")} + +
- + + + + + + + ${_("Use the default customized (subreddit-specific) DART ad frame")} + + + + + + + + + ${_("specify the location of the ad frame:")} + + + + + +
+ + + <%utils:line_field title="${_('sponsorship')}"> +
    +
  • + +
  • +
  • + +
  • +
  • + <%utils:image_upload post_target="/api/upload_sr_img" + current_image="${thing.site.sponsorship_img}" + label="${_('upload sponsorship image')}" + form_id="sponsor-upload"> + +
    + + + + +
  • +
+ + %endif -
+%endif + + +
<% if thing.site: name = "edit" @@ -262,9 +332,13 @@ function update_title(elem) { text = _("create") %> + onclick="return post_pseudo_form('#sr-form', 'site_admin')"> + ${text} + ${error_field("RATELIMIT", "ratelimit")} +
+ + diff --git a/r2/r2/templates/dart_ad.html b/r2/r2/templates/dart_ad.html new file mode 100644 index 000000000..7cdadd1c1 --- /dev/null +++ b/r2/r2/templates/dart_ad.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + diff --git a/r2/r2/templates/link.html b/r2/r2/templates/link.html index b1cdaa244..a05a7f328 100644 --- a/r2/r2/templates/link.html +++ b/r2/r2/templates/link.html @@ -23,6 +23,7 @@ <%! from r2.lib.template_helpers import get_domain from r2.lib.pages.things import LinkButtons + from r2.lib.pages import WrappedUser %> <%inherit file="printable.html"/> @@ -175,7 +176,7 @@ ${unsafe(taglinetext % dict(reddit = self.subreddit(), when = thing.timesince, - author= self.author(attribs = thing.attribs)))} + author= WrappedUser(thing.author, thing.attribs, thing).render()))} <%def name="child()"> diff --git a/r2/r2/templates/link.htmllite b/r2/r2/templates/link.htmllite index 339cfdf26..0722e4021 100644 --- a/r2/r2/templates/link.htmllite +++ b/r2/r2/templates/link.htmllite @@ -20,7 +20,6 @@ ## CondeNet, Inc. All Rights Reserved. ################################################################################ <%namespace file="utils.html" import="optionalstyle"/> -<%namespace file="printable.html" import="score"/> <%! from pylons.i18n import _, ungettext @@ -46,7 +45,12 @@ > %if not expanded: - ${score(thing, thing.likes, tag='span')} + <% + if thing.likes is False: + score, cls = thing.display_score[0], "dislikes" + elif thing.likes is None: + score, cls = thing.display_score[1], "unvoted" + else: + score, cls = thing.display_score[2], "likes" + %> + + ${score} + | %endif ${thing.comment_label} diff --git a/r2/r2/templates/link.mobile b/r2/r2/templates/link.mobile index c8a2636f5..c0e3c1bd5 100644 --- a/r2/r2/templates/link.mobile +++ b/r2/r2/templates/link.mobile @@ -39,7 +39,11 @@ # generates "comment" the imperative verb com_label = _("comment") %> - + diff --git a/r2/r2/templates/link.wired b/r2/r2/templates/link.wired index 0fba14d6b..70a2c7ef4 100644 --- a/r2/r2/templates/link.wired +++ b/r2/r2/templates/link.wired @@ -29,7 +29,7 @@ <%def name="entry()"> ${thing.num} - ${thing.title} + ${thing.title}  (${thing.domain}) ${thing.score} ${ungettext("point", "points", thing.score)} diff --git a/r2/r2/templates/linkinfobar.html b/r2/r2/templates/linkinfobar.html index 6fa85fd80..d7972d1ef 100644 --- a/r2/r2/templates/linkinfobar.html +++ b/r2/r2/templates/linkinfobar.html @@ -19,26 +19,47 @@ ## All portions of the code written by CondeNet are Copyright (c) 2006-2009 ## CondeNet, Inc. All Rights Reserved. ################################################################################ +<%! + import locale + from r2.lib.strings import strings + %> + + <%namespace file="printablebuttons.html" import="state_button" /> <%namespace file="printable.html" import="thing_css_class" /> -
- - %if not thing.a.is_self: - - - %endif - - - - - - - - + + +
+ ${_("this post was submitted on")} + + ${thing.a._date.strftime(thing.datefmt)} + +
+ ${unsafe(strings.person_label % dict(num = locale.format("%d", thing.a.score, True), + persons = ungettext("point", "points", + thing.a.score)))} + <% percent = int(float(thing.a.upvotes) / max(thing.a.upvotes + thing.a.downvotes, 1) * 100) %> + (${percent}% ${_("like it")}) +
+ + + ${unsafe(strings.person_label % dict(num = locale.format("%d", thing.a.upvotes, True), + persons = ungettext("up vote", "up votes", + thing.a.upvotes)))} + + + + ${unsafe(strings.person_label % dict(num = locale.format("%d", thing.a.downvotes, True), + persons = ungettext("down vote", "down votes", + thing.a.downvotes)))} + + +
${_("toolbar link")}${thing.a.tblink}
${_("submitted on")}${thing.a._date.strftime(thing.datefmt)}
${ungettext('point', 'points', 5)}${thing.a.score}
${_("up votes")}${thing.a.upvotes}
${_("down votes")}${thing.a.downvotes}
%if c.user_is_admin: <%include file="adminlinkinfo.html"/> %endif + %if c.user_is_sponsor: <%include file="linkpromoteinfobar.html"/> %endif diff --git a/r2/r2/templates/linkpromoteinfobar.html b/r2/r2/templates/linkpromoteinfobar.html index 3441a134f..2a2b80a61 100644 --- a/r2/r2/templates/linkpromoteinfobar.html +++ b/r2/r2/templates/linkpromoteinfobar.html @@ -26,19 +26,21 @@ <%namespace file="utils.html" import="plain_link" /> %if thing.a.promoted: - - - - - %if thing.a.promoted_by: + %if hasattr(thing.a, "promoted_on"): - + - + + %endif + %if hasattr(thing.a, "unpromoted_on"): + + + + %endif %if thing.a.promote_until: @@ -51,11 +53,4 @@ %endif - %if thing.a.promoted_subscribersonly: - - - - - %endif %endif diff --git a/r2/r2/templates/message.html b/r2/r2/templates/message.html index 85650e51c..c7b28c538 100644 --- a/r2/r2/templates/message.html +++ b/r2/r2/templates/message.html @@ -23,6 +23,7 @@ <%! from r2.lib.filters import edit_comment_filter, safemarkdown from r2.lib.pages.things import MessageButtons + from r2.lib.pages import WrappedUser %> <%inherit file="comment_skeleton.html"/> @@ -47,9 +48,10 @@ taglinetext = _("to %(dest)s from %(author)s sent %(when)s ago") taglinetext = taglinetext.replace(' ', ' ') + author = WrappedUser(thing.author, thing.attribs, thing).render() %> ${unsafe(taglinetext % dict(when = thing.timesince, - author= u"%s" % self.author(thing.attribs), + author= u"%s" % author, dest = u"%s" % thing.to.name))} %if c.user_is_admin: @@ -58,6 +60,17 @@

${thing.subject} + %if thing.was_comment: + ${thing.link_title} + %if hasattr(thing, "parent"): +

+

+ + ${_("show parent")} + + %endif + %endif <%def name="ParentDiv()"> @@ -72,7 +85,7 @@ ${unsafe(safemarkdown(thing.body))} <%def name="entry()"> -

+

${self.tagline()}

diff --git a/r2/r2/templates/messagecompose.html b/r2/r2/templates/messagecompose.html index 78f953756..72f414bb4 100644 --- a/r2/r2/templates/messagecompose.html +++ b/r2/r2/templates/messagecompose.html @@ -24,40 +24,112 @@ <%namespace name="utils" file="utils.html"/> <%! + import simplejson from r2.lib.pages import UserText %>

${_("send a message")}

- <%utils:submit_form onsubmit="return post_form(this, 'compose', null, null, true)", method="post", _id = "compose-message", action="/message/compose"> + +
<%utils:round_field title="${_('to')}", description="${_('(username)')}"> - + ${error_field("NO_USER", "to")} ${error_field("USER_DOESNT_EXIST", "to")} -
+
<%utils:round_field title="${_('subject')}"> ${error_field("NO_SUBJECT", "subject", "span")} -
+
<%utils:round_field title="${_('message')}"> ${UserText(None, have_form = False, creating = True)} -
+ + + +
${thing.captcha} -
+ diff --git a/r2/r2/templates/morechildren.html b/r2/r2/templates/morechildren.html index 3d48b5c81..71d582a20 100644 --- a/r2/r2/templates/morechildren.html +++ b/r2/r2/templates/morechildren.html @@ -33,7 +33,8 @@ -load more comments  (${thing.count} ${ungettext("reply", "replies", thing.count)}) +${_("load more comments")} + (${thing.count} ${ungettext("reply", "replies", thing.count)}) diff --git a/r2/r2/templates/paymentform.html b/r2/r2/templates/paymentform.html new file mode 100644 index 000000000..92d232bd4 --- /dev/null +++ b/r2/r2/templates/paymentform.html @@ -0,0 +1,209 @@ +## The contents of this file are subject to the Common Public Attribution +## License Version 1.0. (the "License"); you may not use this file except in +## compliance with the License. You may obtain a copy of the License at +## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +## License Version 1.1, but Sections 14 and 15 have been added to cover use of +## software over a computer network and provide for limited attribution for the +## Original Developer. In addition, Exhibit A has been modified to be consistent +## with Exhibit B. +## +## Software distributed under the License is distributed on an "AS IS" basis, +## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +## the specific language governing rights and limitations under the License. +## +## The Original Code is Reddit. +## +## The Original Developer is the Initial Developer. The Initial Developer of +## the Original Code is CondeNet, Inc. +## +## All portions of the code written by CondeNet are Copyright (c) 2006-2009 +## CondeNet, Inc. All Rights Reserved. +################################################################################ +<%! + from r2.lib.template_helpers import static +%> +<%namespace file="utils.html" import="error_field"/> + + + + +
+

${_("set up payment for this link")}

+
+ onsubmit="return post_form(this, 'update_pay')"> + + +

+ <% + day = (thing.link.promote_until - thing.link._date).days + %> + The duration of this link is ${day} ${ungettext("day", "days", day)} + (${thing.link._date.strftime("%m/%d/%Y")} - + ${thing.link.promote_until.strftime("%m/%d/%Y")}). +

+

+ <% + bid = unsafe("" % thing.link.promote_bid) + %> + ${unsafe(_("Your current bid is $%(bid)s") % dict(bid=bid))} + ${error_field("BAD_BID", "bid")} + + ${_('(total for the duration provided)')} + +

+ %if thing.profiles: +

+ ${_("Please pick your credit card:")} + +

+ %else: +

+ ${_("please create a new payment profile")} +

+ %endif +

+ ${_("NOTE: your card will not be charged until the link has been queued for promotion.")} +

+ %if thing.link: + + %endif + + + ${profile_info(None, disabled=bool(thing.profiles))} + %for profile in thing.profiles: + ${profile_info(profile, disabled=True)} + %endfor + + + +
+ +<%def name="profile_info(profile, disabled=False)"> + <% + address = ((_("first name") , "firstName", ""), + (_("last name") , "lastName", ""), + (_("company") , "company", _("(optional)")), + (_("address") , "address", ""), + (_("city") , "city", ""), + (_("state") , "state", ""), + (_("zip") , "zip", ""), + ## (_("country") , "country", ""), + (_("phone") , "phoneNumber", _("(optional)"))) + cc = ((_("card number") , "cardNumber", _("(14-17 digits)")), + (_("expiration date") , "expirationDate", "(YYYY-MM please)"), + (_("CCV") , "cardCode", _("(3 or 4 digits)"))) + bill_to = getattr(profile, "billTo",None) + credit = getattr(profile, "payment", None) + credit = getattr(credit, "creditCard", None) + prof_id = getattr(profile, "customerPaymentProfileId", "") + display = "style='display:none'" if disabled else '' + disabled = "disabled" if disabled else "" + %> +
+ %if profile: + + %endif + +
${_('promoted on')} - ${thing.a.promoted_on.strftime(thing.datefmt)} -
${_('promoted by')}${_('promoted on')} - ${thing.a.promoted_by_name} + ${thing.a.promoted_on.strftime(thing.datefmt)}
${_('unpromoted on')} + ${thing.a.unpromoted_on.strftime(thing.datefmt)} +
${(_('shown only to subscribers of %(subreddit)s') - % dict(subreddit = c.site.name))}
+ %for fields, data, error_name in ((address, bill_to, "BAD_ADDRESS"), (cc, credit, "BAD_CARD")): + %for label, field, optional in fields: + + + + + + %endfor + %endfor + + + + + + + + +
+ %if field == "address": + + %elif field == "country": + ## TODO: pycountry does country name i18n + + %else: + + %endif + %if optional: + ${optional} + %endif + + ${error_field(error_name, field)} +
+ + %if disabled and profile: + + %endif +
+ +
+
+ diff --git a/r2/r2/templates/prefoptions.html b/r2/r2/templates/prefoptions.html index f396160ff..f0b2adcd7 100644 --- a/r2/r2/templates/prefoptions.html +++ b/r2/r2/templates/prefoptions.html @@ -170,6 +170,11 @@ ${_("display options")} ${checkbox(_("allow reddits to show me custom styles"), "show_stylesheets")} + %if c.user.pref_show_promote is not None: +
+ ${checkbox(_("show promote tab on front page"), + "show_promote")} + %endif diff --git a/r2/r2/templates/prefupdate.html b/r2/r2/templates/prefupdate.html index 9d77cf278..a00b48b7c 100644 --- a/r2/r2/templates/prefupdate.html +++ b/r2/r2/templates/prefupdate.html @@ -23,7 +23,19 @@ <%namespace file="utils.html" import="error_field"/> <%namespace name="utils" file="utils.html"/> -

${_("update your email or password")}

+

+%if thing.email and thing.password: + ${_("update your email or password")} +%elif thing.email: + %if thing.verify: + ${_("verify your email")} + %else: + ${_("update your email")} + %endif +%else: + ${_("update your password")} +%endif +

@@ -35,13 +47,25 @@ +%if thing.email:
- <%utils:round_field title="${_('email')}"> + <% + if c.user.email_verified: + description = _("verified") + elif c.user.email_verified is False: + description = _("verification pending") + else: + description = _("unverified") + description = "(%s)" % description + %> + <%utils:round_field title="${_('email')}" description="${description}"> ${error_field("BAD_EMAILS", "email")}
+%endif +%if thing.password:
<%utils:round_field title="${_('new password')}"> @@ -55,8 +79,13 @@ ${error_field("BAD_PASSWORD_MATCH", "verpass")}
+%endif - +%if thing.verify and not c.user.email_verified: + + +%else: + +%endif -
diff --git a/r2/r2/templates/printable.html b/r2/r2/templates/printable.html index 82c9a5044..9cead8440 100644 --- a/r2/r2/templates/printable.html +++ b/r2/r2/templates/printable.html @@ -21,7 +21,7 @@ ################################################################################ <%! - from r2.lib.template_helpers import add_sr, find_author_class + from r2.lib.template_helpers import add_sr from r2.lib.strings import strings from r2.lib.pages.things import BanButtons %> @@ -115,57 +115,6 @@ thing id-${what._fullname} -<%def name="author(attribs = None, gray = False)" buffered="True"> - %if thing.deleted and not c.user_is_admin: - [deleted] - %else: - <% - attribs.sort() - author_cls = find_author_class(thing, attribs, gray) - - target = getattr(thing, "target", None) - - disp_name = websafe(thing.author.name) - karma = "" - if c.user_is_admin: - karma = " (%d)" % (thing.author.link_karma) - %> - - %if thing.author._deleted: - [deleted] - %else: - ${plain_link(disp_name + karma, "/user/%s" % disp_name, - _class = author_cls, _sr_path = False, target=target)} - %if attribs: - - [ - %for priority, abbv, css_class, label, attr_link in attribs: - %if attr_link: - ${abbv} - %else: - ${abbv} - %endif - ## this is a hack to print a comma after all but the final attr - %if priority != attribs[-1][0]: - , - %endif - %endfor - ] - - %endif - %endif - %endif - - %if c.user_is_admin and hasattr(thing, 'ip') and thing.ip: - - %endif - - - <%def name="arrow(this, dir, mod)"> <% _type = "up" if dir > 0 else "down" diff --git a/r2/r2/templates/printablebuttons.html b/r2/r2/templates/printablebuttons.html index c71ffd3f9..1dec1bf3a 100644 --- a/r2/r2/templates/printablebuttons.html +++ b/r2/r2/templates/printablebuttons.html @@ -22,6 +22,7 @@ <%namespace file="utils.html" import="plain_link" /> <%! from r2.lib.strings import strings + from r2.lib.promote import STATUS %> <%def name="banbuttons()"> @@ -104,7 +105,7 @@ %endif %if thing.editable:
  • - ${self.simple_button(_("edit"), "edit_usertext")} + ${self.simple_button(_("edit"), "edit_usertext", css_class="edit-usertext")}
  • %endif %endif ${self.banbuttons()} + %if thing.promoted is not None: + %if thing.promote_status != STATUS.finished or c.user_is_sponsor: + %if thing.user_is_sponsor or thing.is_author: +
  • + ${plain_link(_("edit"), thing.promo_url, _sr_path = False)} +
  • +
  • + ${plain_link(_("traffic"), thing.traffic_url, _sr_path = False)} +
  • + %if thing.promote_status == STATUS.promoted: +
  • + ${ynbutton(_("unpromote"), _("unpromoted"), "unpromote")} +
  • + %elif c.user_is_sponsor and thing.promote_status != STATUS.rejected: +
  • + + ${toggle_button("reject_promo", \ + _("reject"), _("cancel"), \ + "reject_promo", "cancel_reject_promo")} +
  • + %endif + %endif + %if thing.user_is_sponsor: + %if thing.promote_status in (STATUS.unseen, STATUS.rejected): +
  • + ${ynbutton(_("accept"), _("accepted"), "promote")} +
  • + %elif thing.promotable and thing.promote_status in (STATUS.accepted, STATUS.pending): +
  • + ${ynbutton(_("promote"), _("promote"), "promote")} +
  • + %endif + %endif + %endif + %endif ${self.distinguish()} @@ -153,7 +201,7 @@ %endif %if thing.is_author:
  • - ${self.simple_button(_("edit"), "edit_usertext")} + ${self.simple_button(_("edit"), "edit_usertext", css_class="edit-usertext")}
  • %endif %endif @@ -175,9 +223,9 @@ %endif ${self.banbuttons()} - %if not thing.was_comment and thing.can_reply: + %if thing.can_reply:
  • - ${self.simple_button(_("reply {verb}"), "reply")} + ${self.simple_button(_("reply {verb}"), "reply")}
  • %endif @@ -250,8 +298,9 @@ -<%def name="simple_button(title, nameFunc)"> - + ${title} @@ -275,9 +324,19 @@ onclick="return toggle(this, ${callback}, ${cancelback})" %endif > - ${title} + %if title: + ${title} + %else: +   + %endif + + + %if alt_title: + ${alt_title} + %else: +   + %endif - ${alt_title} diff --git a/r2/r2/templates/profilebar.html b/r2/r2/templates/profilebar.html index 1fa304cb0..7133077e7 100644 --- a/r2/r2/templates/profilebar.html +++ b/r2/r2/templates/profilebar.html @@ -21,76 +21,94 @@ ################################################################################ <%! + import locale from r2.lib.filters import edit_comment_filter, unsafe, safemarkdown + from r2.lib.template_helpers import static from r2.lib.utils import timesince %> <%namespace file="utils.html" import="submit_form, plain_link"/> <%namespace file="printablebuttons.html" import="toggle_button"/> -<% user = thing.user %> -%if thing.user: -
    -

    ${thing.user.name}

    -
      +
      +

      + ${thing.user.name} + %if c.user_is_admin: - %if thing.user._spam: -
    • (banned)
    • - %endif -
    • - - <% - karmas = thing.user.all_karmas() - %> - %for i, (label, lc, cc) in enumerate(karmas): - - - - - - - %endfor -
      ${label}:${lc}/${cc}
      - %if i >= 5: - - show karma for all ${len(karmas)} reddits - - %endif -
    • - %else: -
    • - ${_("karma:")} ${thing.user.safe_karma} -
    • -
    • - ${_("comment karma:")} ${thing.user.comment_karma} -
    • - %endif -
    • - ${_("user for %(time)s") % dict(time=timesince(thing.user._date))} -
    • + + %endif + +

      + + %if c.user != thing.user: +
      + ${toggle_button("fancy-toggle-button", _("+ friends"), _("- friends"), + "friend('%s', '%s', 'friend')" % (thing.user.name, thing.my_fullname), + "unfriend('%s', '%s', 'friend')" % (thing.user.name, thing.my_fullname), + css_class = "add", alt_css_class = "remove", + reverse = thing.is_friend)} +
      %endif -
    -
    + %if c.user_is_admin: + + <% + karmas = thing.user.all_karmas() + %> + %for i, (label, lc, cc) in enumerate(karmas): + + + + + + + %endfor +
    ${label}:${lc}/${cc}
    + %if i >= 5: + + show karma for all ${len(karmas)} reddits + + %endif + %else: + ${locale.format("%d", thing.user.safe_karma, True)} + + ${_("link karma")} + +
    + ${locale.format("%d", thing.user.comment_karma, True)} + + ${_("comment karma")} + %endif + +
    + %if thing.user != c.user: + + + ${plain_link(_("send message"), "/message/compose/?to=%s" % thing.user.name)} + %endif + + + ${_("redditor for %(time)s") % dict(time=timesince(thing.user._date))} + +
    + +
    - -%endif diff --git a/r2/r2/templates/promo_email.email b/r2/r2/templates/promo_email.email new file mode 100644 index 000000000..2f761059d --- /dev/null +++ b/r2/r2/templates/promo_email.email @@ -0,0 +1,118 @@ +## The contents of this file are subject to the Common Public Attribution +## License Version 1.0. (the "License"); you may not use this file except in +## compliance with the License. You may obtain a copy of the License at +## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +## License Version 1.1, but Sections 14 and 15 have been added to cover use of +## software over a computer network and provide for limited attribution for the +## Original Developer. In addition, Exhibit A has been modified to be consistent +## with Exhibit B. +## +## Software distributed under the License is distributed on an "AS IS" basis, +## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +## the specific language governing rights and limitations under the License. +## +## The Original Code is Reddit. +## +## The Original Developer is the Initial Developer. The Initial Developer of +## the Original Code is CondeNet, Inc. +## +## All portions of the code written by CondeNet are Copyright (c) 2006-2009 +## CondeNet, Inc. All Rights Reserved. +################################################################################ +<%! + from datetime import datetime, timedelta + from r2.models import Email + from r2.lib import promote + from r2.lib.template_helpers import get_domain + %> +<% + edit_url = promote.promo_edit_url(thing.link) + %> + +%if thing.kind == Email.Kind.NEW_PROMO: +This email is to confirm reddit.com's receipt of your submitted self-serve ad. To set up payment for your ad, please go here: + +${g.payment_domain}promoted/pay/${thing.link._id36} + +Please note that we can't approve your ad until you have set up payment, and that your ad must be approved before it goes live on your selected dates. + +Your credit card will not be charged until 24 hours prior to your ad going live on reddit. + +If your ad is rejected your credit card will not be charged. Don't take it personally if it happens, it's because of these guidelines: + +http://www.reddit.com/help/selfservicepromotion + +%elif thing.kind == Email.Kind.BID_PROMO: +This email is to confirm that your bid of $${"%.2f" % thing.link.promote_bid} for a self-serve ad on reddit.com has been accepted. The credit card number you provided will be charged 24 hours prior to the date your self-serve ad is set to run. + +Having second thoughts about your bid? Want to be sure you're outbidding the competition? You'll have until ${(thing.link._date - timedelta(1)).strftime("%Y-%m-%d")} to change your bid here: + + ${edit_url} +%elif thing.kind == Email.Kind.ACCEPT_PROMO: +This email is to confirm that your self-serve reddit.com ad has been approved by reddit! The credit card you provided will not be charged until 24 hours prior to the date you have set your ad to run. If you make any changes to your ad, they will have to be re-approved. Keep in mind that after we have charged your credit card you will not be able to change your ad. + +It won't be long now until your ad is being displayed to hundreds of thousands of the Internet's finest surfers. + +%elif thing.kind == Email.Kind.REJECT_PROMO: +This email is to inform you that the self-serve ad you submitted to reddit.com has been rejected. :( Please view the included explanation, and consult reddit's Terms of Service for information on conditions for approval. If you have any questions please reply to this email. + +%if thing.body: +Your promotion has been rejected for the following reason: + ${thing.body} +%endif: + +To update your promotion please go to: + ${edit_url} +and we'll reconsider it for sumbission. +%elif thing.kind == Email.Kind.QUEUED_PROMO: +This email is to inform you that your self-serve ad on reddit.com is about to go live. Feel free to reply to this email if you have any questions. + +%if thing.link.promote_trans_id > 0: + Your credit card has been successfully charged by reddit for the amount you bid. Please use this email as your receipt. + + +================================================================================ +TRANSACTION #${thing.link.promote_trans_id} +DATE: ${datetime.now(g.tz).strftime("%Y-%m-%d")} +................................................................................ + +AMOUNT CHARGED: $${"%.2f" % thing.link.promote_bid} +SPONSORSHIP PERMALINK: ${thing.link.make_permalink_slow(force_domain = True)} + +================================================================================ +%else: +Your promotion was a freebie in the amount of $${"%.2f" % thing.link.promote_bid}. +%endif +%elif thing.kind == Email.Kind.LIVE_PROMO: +This email is to inform you that your self-serve ad on reddit.com is now live and can be found at the following link: + + ${thing.link.make_permalink_slow(force_domain = True)} + +Thank you for your business! You can track your promotion's traffic here: + + ${promote.promo_traffic_url(thing.link)} + +Note that there is a 2 hour delay on tracking so at first you make not see any data, and completed traffic will be 2-3 hours behind. + +Remember to log in to reddit.com using the username and password you used when you bought this self-serve ad. Please let us know if you have any questions. +%elif thing.kind == Email.Kind.FINISHED_PROMO: +This email is to inform you that your self-serve ad on reddit.com has concluded. Please visit the following link for traffic results for your ad, and note that complete results will not appear until 24 hours after receipt of this email. + + ${promote.promo_traffic_url(thing.link)} + +Remember to log in to reddit.com using the username and password you used when you bought this self-serve ad. + +Thank you again for advertising on reddit, we hope you'll come back and do business with us again. We'd love to know how your experience with reddit's self-serve ad was, feel free to reply to this email to let us know. We've also set up a community just for self-serve advertisers like yourself: + + http://www.reddit.com/r/selfserve + +We're hoping to create a place for you to exchange tips and tricks for getting the most out of your sponsored links, as well as to provide support for new users. +%endif + +Thank you, + +The Reddit Team +selfservicepromotion@reddit.com + +_____ +http://www.reddit.com/help/selfservicepromotion \ No newline at end of file diff --git a/r2/r2/templates/promote_graph.html b/r2/r2/templates/promote_graph.html new file mode 100644 index 000000000..a640c5e6d --- /dev/null +++ b/r2/r2/templates/promote_graph.html @@ -0,0 +1,276 @@ +## The contents of this file are subject to the Common Public Attribution +## License Version 1.0. (the "License"); you may not use this file except in +## compliance with the License. You may obtain a copy of the License at +## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +## License Version 1.1, but Sections 14 and 15 have been added to cover use of +## software over a computer network and provide for limited attribution for the +## Original Developer. In addition, Exhibit A has been modified to be consistent +## with Exhibit B. +## +## Software distributed under the License is distributed on an "AS IS" basis, +## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +## the specific language governing rights and limitations under the License. +## +## The Original Code is Reddit. +## +## The Original Developer is the Initial Developer. The Initial Developer of +## the Original Code is CondeNet, Inc. +## +## All portions of the code written by CondeNet are Copyright (c) 2006-2009 +## CondeNet, Inc. All Rights Reserved. +################################################################################ +<%! + import datetime, locale + def num(x): + return locale.format('%d', x, True) + def money(x): + return "$%.2f" % x + %> +

    Sponsored link calendar

    + +%if not c.user_is_sponsor: +
    +

    Below is a calendar of your scheduled and completed promotions (if you have any, of course), along with some site-wide averages to use as a guide for setting up future promotions. These values are:

    +
    +
    Count:
    +
    Total number of sponsored links which either ran or are scheduled to run (site-wide).
    +
    CPM:
    +
    Cost per thousand impressions (site-wide). Note that today's CPM is estimated.
    +
    CPC:
    +
    Cost per click, also site-wide and with the same caveat about today's value
    +
    your commit:
    +
    Total amount you have spent or have scheduled to spend on a given day
    +
    +
    + +%endif + +<% max_percent = 97. %> + +
    + <% + today = thing.now.date() + %> +
    +
    + DATE +
    +
    + COUNT +
    +
    + %if c.user_is_sponsor: + IMPRESSIONS
    + %endif + CPM +
    +
    + %if c.user_is_sponsor: + CLICKS
    + %endif + CPC +
    +
    + %if c.user_is_sponsor: + TOTAL COMMIT + %else: + YOUR COMMIT + %endif +
    +
    + %for i in xrange(thing.total_size): + <% + left = "%.2f" % (max_percent*float(i+1)/(thing.total_size+1)) + right = "%.2f" % (100 - max_percent*float(i+2)/(thing.total_size+1)) + day = thing.start_date + datetime.timedelta(i) + CPC = CPM = imp_traffic = cli_traffic = "---" + if thing.promo_traffic.has_key(day): + imp_traffic, cli_traffic = thing.promo_traffic[day] + if thing.market.has_key(i): + CPM = "$%.2f" % (thing.market[i] * 1000./max(imp_traffic, 1)) + CPC = "$%.2f" % (thing.market[i] * 1./max(cli_traffic, 1)) + imp_traffic = num(imp_traffic) + cli_traffic = num(cli_traffic) + if day == today: + imp_traffic = "(%s)" % imp_traffic + cli_traffic = "(%s)" % cli_traffic + %> +
    +
    + ${day} +
    +
    + ${thing.promo_counter.get(i, unsafe(" "))} +
    +
    + %if c.user_is_sponsor: + ${imp_traffic}
    + %endif + ${CPM} +
    +
    + %if c.user_is_sponsor: + ${cli_traffic}
    + %endif + ${CPC} +
    +
    + ${"$%.2f" % thing.my_market[i] if thing.my_market.has_key(i) else "---"} +
    +
    + %endfor + +<% + prev_end = 0 + %> +%for link, start, end in thing.promote_blocks: + <% + start += 1 + end += 1 + %> + %if start != end: + %if prev_end > start: + <% prev_end = 0 %> +
    + %endif + <% + margin = "%.2f" % (float(max_percent*(start-prev_end))/(thing.total_size+1)) + width = "%.2f" % (float(max_percent*(end-start))/(thing.total_size+1)) + prev_end = end + %> + + %endif +%endfor +
    +
    + +%if c.user_is_sponsor: +

    ${_("total promotion traffic")}

    +
    + %if thing.imp_graph: + impressions graph + %endif + %if thing.cli_graph: + click graph + %endif + %if thing.money_graph: + money graph + %endif +
    +%endif + +

    ${_("historical site performance")}

    +
    +%if thing.cpm_graph: + CPM graph +%endif +%if thing.cpc_graph: + CPC graph +%endif +%if thing.cpc_graph and c.user_is_sponsor: + CTR graph +%endif +
    + +%if c.user_is_sponsor: +
    + %if thing.top_promoters: +

    ${_('top promoters this month')}

    + + + + + + + + + + + %for account, bid, refund, promos in thing.top_promoters: + + + + + + + + + %endfor + %if len(thing.top_promoters) != 1: + <% + totals = zip(*thing.top_promoters) + total_bid = sum(totals[1]) + total_refund = sum(totals[2]) + total_promos = sum(map(len, totals[3])) + %> + + + + + + + + + %endif +
    userpromosbidscreditstotalper promo
    ${account.name}${len(promos)}${money(bid)}${money(refund)}${money(bid - refund)}${money((bid - refund)/len(promos))}
    Total${total_promos}${money(total_bid)}${money(total_refund)}${money(total_bid - total_refund)}${money((total_bid - total_refund)/total_promos)}
    + %endif + + %if thing.recent: +

    ${_('promotions this month')} [CSV]

    + + + + + + + + + + + + + + + + + + + %for link, uimp, nimp, ucli, ncli in thing.recent: + + + + + + + + + + + %endfor +
    ImpresionsClicks
    dateuniquetotaluniquetotalpricepointstitle
    + ${link._date.strftime("%Y-%m-%d")} + ${num(uimp)}${num(nimp)}${num(ucli)}${num(ncli)}${money(link.promote_bid)}${link._ups - link._downs} + ${link.title} +
    + %endif +%endif diff --git a/r2/r2/templates/promotedlink.html b/r2/r2/templates/promotedlink.html index 48bafd2f1..7e7e3d3b6 100644 --- a/r2/r2/templates/promotedlink.html +++ b/r2/r2/templates/promotedlink.html @@ -20,7 +20,9 @@ ## CondeNet, Inc. All Rights Reserved. ################################################################################ <%! - from r2.lib.utils import to36 + from r2.lib.promote import promo_edit_url, STATUS + from r2.lib.pages.things import LinkButtons + from r2.lib.pages import WrappedUser %> <%inherit file="link.html"/> @@ -28,36 +30,59 @@ <%namespace file="utils.html" import="plain_link" /> <%def name="tagline()"> +<% + if thing.promote_status < STATUS.promoted and (c.user_is_sponsor or thing.is_author): + taglinetext = _("to be promoted on %(date)s by %(author)s") + else: + taglinetext = _("promoted %(when)s ago by %(author)s") + taglinetext = taglinetext.replace(" ", " ") + author = WrappedUser(thing.author, thing.attribs, thing).render() + %> +${unsafe(taglinetext % dict(when = thing.timesince, + date = thing._date.strftime("%Y-%m-%d"), + author= author))} -<%def name="unpromote_button()" buffered="True" filter="unsafe"> - %if c.user_is_sponsor: -
  • - ${plain_link(_('edit'),'/promote/edit_promo/%s' % to36(thing._id), - _sr_path = False)} -
  • -
  • - ${ynbutton(_("unpromote"), _("unpromoted"), "unpromote")} -
  • - %endif - - -<%def name="buttons()"> - ${parent.buttons(comments=not thing.disable_comments, - report=False, - additional=unpromote_button())} +<%def name="buttons(comments=True, delete=True, report=True, additional='')"> + ${LinkButtons(thing, + comments = not getattr(thing, "disable_comments", False), + delete = delete, + report = report)} <%def name="entry()"> ${parent.entry()} -  - + %if thing.promote_status == STATUS.promoted: +  + + %endif diff --git a/r2/r2/templates/promotedlinks.html b/r2/r2/templates/promotedlinks.html deleted file mode 100644 index 3253928c5..000000000 --- a/r2/r2/templates/promotedlinks.html +++ /dev/null @@ -1,83 +0,0 @@ -## The contents of this file are subject to the Common Public Attribution -## License Version 1.0. (the "License"); you may not use this file except in -## compliance with the License. You may obtain a copy of the License at -## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public -## License Version 1.1, but Sections 14 and 15 have been added to cover use of -## software over a computer network and provide for limited attribution for the -## Original Developer. In addition, Exhibit A has been modified to be consistent -## with Exhibit B. -## -## Software distributed under the License is distributed on an "AS IS" basis, -## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -## the specific language governing rights and limitations under the License. -## -## The Original Code is Reddit. -## -## The Original Developer is the Initial Developer. The Initial Developer of -## the Original Code is CondeNet, Inc. -## -## All portions of the code written by CondeNet are Copyright (c) 2006-2009 -## CondeNet, Inc. All Rights Reserved. -################################################################################ -<%! - from r2.lib.utils import to36 -%> -<%namespace file="utils.html" import="plain_link"/> -<%namespace file="printablebuttons.html" import="ynbutton"/> - - -
    -

    ${_('current promotions')}

    - - - - %if thing.recent: -

    ${_('promotions this month')}

    - - - - - - - - - - - - - - - - - %for link, uimp, nimp, ucli, ncli in thing.recent: - - - - - - - - - %endfor -
    ImpresionsClicks
    uniquetotaluniquetotalpointstitle
    ${uimp}${nimp}${ucli}${ncli}${link._ups - link._downs} - ${link.title} -
    - %endif -
    diff --git a/r2/r2/templates/promotedtraffic.html b/r2/r2/templates/promotedtraffic.html index 79fdfabe4..e2bcc6413 100644 --- a/r2/r2/templates/promotedtraffic.html +++ b/r2/r2/templates/promotedtraffic.html @@ -25,8 +25,26 @@ def num(x): return locale.format('%d', x, True) %> + diff --git a/r2/r2/templates/promotelinkform.html b/r2/r2/templates/promotelinkform.html index 03841452c..170c164a0 100644 --- a/r2/r2/templates/promotelinkform.html +++ b/r2/r2/templates/promotelinkform.html @@ -22,119 +22,320 @@ <%! from r2.lib.utils import to36 from r2.lib.media import thumbnail_url + from r2.lib.template_helpers import static + from r2.lib.promote import STATUS %> -<%namespace file="utils.html" import="error_field, checkbox, plain_link, image_upload" /> +<%namespace file="utils.html" + import="error_field, checkbox, plain_link, image_upload" /> <%namespace file="printablebuttons.html" import="ynbutton"/> -<%namespace file="utils.html" import="error_field, checkbox, image_upload" /> +<%namespace name="utils" file="utils.html"/> + + + + + + + +<%def name="datepicker(name = 'date', value = '', minDateSrc = '', maxDateSrc ='')"> +
    + +
    + +
    + + + +
    + +<% + title = _("create a promotion") if not thing.link else _("edit promotion") + %> +

    ${title}

    %if thing.link: ${thing.listing} %endif +
    + +%if thing.link and not c.user_is_sponsor: + %if thing.link.promote_status == STATUS.unpaid: +
    +
    + ${_('NOTE:')} +
      +
    1. + Please set up payment to authorize payment of this bid. +
    2. +
    3. + ${_('once you set up payment, you will not be charged until the link is approved and scheduled for display')} +
    4. +
    +
    +
    + %elif thing.link.promote_status == STATUS.rejected: +
    +
    + ${_("This promotion has been rejected. Please edit and resubmit.")} +
    +
    + %elif thing.link.promote_status == STATUS.unseen: +
    +
    + ${_("Your bid has been registered and your submission is awaiting review. We will notify you by email of status updates.")} +
    +
    + %elif thing.link.promote_status < STATUS.finished: +
    +
    + ${_("NOTE: changes to this promotion will result in its status being reverted to 'unapproved'")} +
    +
    + %else: +
    +
    + ${_("This promotion is finished. Edits would be a little pointless.")} +
    +
    + %endif +%endif +
    + id="promo-form" + onsubmit="return post_form(this, 'new_promo')"> %if thing.link: %endif - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - ${error_field("NO_TEXT", "title")} - ${error_field("TOO_LONG", "title")} -
    - - - ${error_field("NO_URL", "url")} - ${error_field("BAD_URL", "url")} - ${error_field("ALREADY_SUB", "url")} -
    - %if thing.link: - ${checkbox("subscribers_only", - (_("show only to subscribers of %(reddit)s") - % dict(reddit = thing.sr.name)), - thing.link.promoted_subscribersonly)} - + <%utils:line_field title="${_('title')}" id="title-field"> + + ${error_field("NO_TEXT", "title", "div")} + ${error_field("TOO_LONG", "title", "div")} + + + <%utils:line_field title="${_('url')}" id="url-field"> + + + ${error_field("NO_URL", "url", "div")} + ${error_field("BAD_URL", "url", "div")} + ${error_field("ALREADY_SUB", "url", "div")} +
    + + +
    + + + <%utils:line_field title="${_('duration')}" id="duration-field"> + %if (thing.link and thing.link.promote_status >= STATUS.pending) and not c.user_is_sponsor: +
    + + + ${thing.link._date.strftime(thing.datefmt)} - + ${thing.link.promote_until.strftime(thing.datefmt)} +
    %else: -
    - ${checkbox("subscribers_only", - _("show only to subscribers of this reddit"), - False)} + %if not c.user_is_sponsor: + %endif -
    - ${error_field("SUBREDDIT_NOEXIST", "sr")} -
    ${_("site options")} + <%self:datepicker name="startdate", value="${thing.startdate}" + minDateSrc="#date-min"> + function(elem) { + var other = $("#enddate"); + if(dateFromInput("#startdate") >= dateFromInput("#enddate")) { + var newd = new Date(); + newd.setTime($(elem).datepicker('getDate').getTime() + 86400*1000); + $("#enddate").val((newd.getMonth()+1) + "/" + + newd.getDate() + "/" + newd.getFullYear()); + } + $("#datepicker-enddate").datepicker("destroy"); + update_bid(elem); + } + + - + <%self:datepicker name="enddate", value="${thing.enddate}" + minDateSrc="#startdate"> + function(elem) { update_bid(elem); } + + %endif + ${error_field("BAD_DATE", "sr")} + ${error_field("BAD_FUTURE_DATE", "sr")} + ${error_field("BAD_PAST_DATE", "sr")} + ${error_field("BAD_DATE_RANGE", "sr")} + + + <%utils:line_field title="${_('options')}" id="commenting-field"> +
    + <% + clicks = views = 0 + disable_comments = False + if thing.link: + disable_comments = getattr(thing.link, "disable_comments", + False) + clicks = getattr(thing.link, "maximum_clicks", 0) or 0 + views = getattr(thing.link, "maximum_views", 0) or 0 + %> ${checkbox("disable_comments", - _("disable comments"), - thing.link.disable_comments if thing.link else False)}
    -
    - %if thing.link and thing.link.promote_until: - - ${(_('expire in %(timedelta)s (%(expires_at)s)') - % dict(timedelta = thing.timedeltatext, - expires_at = thing.link.promote_until.strftime(thing.datefmt)))}
    - - ${_("expire in")}   - -
    - - ${_("don't expire")}
    - %else: - - ${_("don't expire")}
    - - ${_("expire in")}   - - + _("disable comments"), disable_comments)}
    + %if c.user_is_sponsor: + ${checkbox("set_maximum_clicks", + unsafe("maximum clicks: " + + "" % clicks), + clicks)}
    + ${checkbox("set_maximum_views", + unsafe("maximum views: " + + "" % views), + views)}
    %endif -
    ${error_field("BAD_NUMBER", "timelimitlength")}
    +
    + + +%if not thing.link or (c.user_is_sponsor and thing.link.promote_status < STATUS.pending): + <%utils:line_field title="${_('bid amount')}" id="bid-field" + description="${_('(total for the duration provided)')}"> + $ + %if getattr(thing.link, "promote_trans_id", 0) < 0: + + ${_("This link is currently a freebie.")} + + %endif + ${error_field("BAD_BID", "bid")} + +%else: -%if thing.link: - <% thumb = None if not thing.link.has_thumbnail else thumbnail_url(thing.link) %> -<%call expr="image_upload('/api/link_thumb', thumb, tabular=True, label=_('thumbnail'))"> - - - + + + <%utils:line_field title="${_('bid amount')}" id="bid-field" + description="${_('(total for the duration provided)')}"> + $${"%.2f" % thing.link.promote_bid} + <% + trans_id = getattr(thing.link, "promote_trans_id", None) + %> + %if trans_id and trans_id < 0: + + ${_("This link is currently a freebie. Enjoy")} + + %elif thing.link.promote_status >= STATUS.pending: + + ${_("this amount has already been charged")} + + %else: + %if thing.link.promote_status == STATUS.unpaid: + + ${_("unpaid")} + + %endif + + we support Visa, MC, Amex, and Discover + %endif + %endif +## provide a way to give refunds, but only if the link isn't already a freebie +<% + cur_bid = getattr(thing.link, "promote_bid", g.min_promote_bid) + cur_refund = cur_bid - getattr(thing.link, "promo_refund", 0) +%> +%if c.user_is_sponsor and getattr(thing.link, "promote_paid", False) and \ + getattr(thing.link, "promote_trans_id", -1) > 0 and cur_refund > 0: + <%utils:line_field title="${_('refund')}" id="bid-field"> +
    + + + $ + ${error_field("BAD_NUMBER", "bid")} + +
    + +%endif + +%if not thing.link or thing.link.promote_status != STATUS.finished: + %if thing.link: + <% + thumb = None if not thing.link.has_thumbnail else thumbnail_url(thing.link) %> + <%utils:line_field title="${_('look and feel')}"> +
    + <%utils:image_upload post_target="/api/link_thumb" + current_image="${thumb}" + label="${_('upload header image:')}"> + + ## overwrite the completed image function + + +
    +
    + + %else: +
    +
    + ${_("You'll be able to submit an image for the thumbnail once the promotion is submitted.")} +
    +
    +
    +
    + By clicking "agree" you agree to the Self Serve Advertising Rules. +
    +
    + %endif +%endif + +
    <% if thing.link: @@ -142,12 +343,84 @@ text = _("save options") else: name = "create" - text = _("create") + text = _("agree") %> - + ${error_field("RATELIMIT", "ratelimit")} + %if thing.link and thing.link.promote_status < STATUS.pending: + %if c.user_is_sponsor and getattr(thing.link, "promote_trans_id", 0) >= 0: +
    + + + +
    + %endif + %endif ${error_field("RATELIMIT", "ratelimit")} + +
    +
    + +%if thing.link and c.user_is_sponsor: +
    + %if thing.bids: + <%utils:line_field title="${_('bidding history')}"> + <% + from r2.models import Account, bidding + accounts = Account._byID(set(x.account_id for x in thing.bids), True) + %> + + + + + + + + + + %for bid in thing.bids: + <% + status = bidding.Bid.STATUS.name[bid.status].lower() + %> + + + + + + + + + %endfor +
    dateusertransaction idpay idamountstatus
    ${bid.date}${accounts[bid.account_id].name}${bid.transaction}${bid.pay_id}$${"%.2f" % bid.bid}${status}
    + + %endif + +
    + <%utils:line_field title="${_('promotion history')}"> + + + + +
    + %for line in getattr(thing.link, "promotion_log", []): +

    ${line}

    + %endfor +
    + +
    + +
    +%endif + diff --git a/r2/r2/templates/reddit.html b/r2/r2/templates/reddit.html index de00cbcba..52fc86aba 100644 --- a/r2/r2/templates/reddit.html +++ b/r2/r2/templates/reddit.html @@ -21,8 +21,8 @@ ################################################################################ <%! - from r2.lib.template_helpers import add_sr, static, join_urls, class_dict, path_info, get_domain - from r2.lib.pages import SearchForm, ClickGadget + from r2.lib.template_helpers import add_sr, static, join_urls, class_dict, get_domain + from r2.lib.pages import SearchForm, ClickGadget, SideContentBox from r2.lib import tracking from pylons import request from r2.lib.strings import strings @@ -54,18 +54,17 @@ %else: - %if c.allow_styles: %if c.site.stylesheet: %endif - %if c.site.stylesheet_contents: - - %endif - %endif + %endif + + %if c.allow_styles and c.site.stylesheet_contents: + %endif %if getattr(thing, "additional_css", None): <%def name="javascript_run()"> - reddit.where = ${path_info()}; reddit.cur_site = "${c.site._fullname if hasattr(c.site, '_fullname') else ''}"; @@ -166,12 +164,14 @@ %if c.user_is_admin: <%include file="admin_rightbox.html"/> - %else: - <%include file="ads.html"/> %endif + <%include file="ads.html"/> + ##cheating... we should move ads into a template of its own - ${ClickGadget(c.recent_clicks)} + %if c.user.pref_clickgadget and c.recent_clicks: + ${SideContentBox(_("Recently viewed links"), [ClickGadget(c.recent_clicks)])} + %endif diff --git a/r2/r2/templates/redditfooter.html b/r2/r2/templates/redditfooter.html index d435aeb6a..d01bfd81f 100644 --- a/r2/r2/templates/redditfooter.html +++ b/r2/r2/templates/redditfooter.html @@ -28,7 +28,7 @@