Skip to content
This repository
Browse code

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
  • Loading branch information...
commit bf9f43ccacc4ee7933629c621f97d6ff0ddd11b8 1 parent 1f1f060
Christopher Slowe authored December 01, 2009

Showing 195 changed files with 17,248 additions and 3,174 deletions. Show diff stats Hide diff stats

  1. 18  r2/Makefile
  2. 87  r2/draw_load.py
  3. 37  r2/example.ini
  4. 6  r2/r2/config/middleware.py
  5. 35  r2/r2/config/routing.py
  6. 2  r2/r2/config/templates.py
  7. 2  r2/r2/controllers/__init__.py
  8. 449  r2/r2/controllers/api.py
  9. 54  r2/r2/controllers/awards.py
  10. 10  r2/r2/controllers/embed.py
  11. 7  r2/r2/controllers/error.py
  12. 12  r2/r2/controllers/errors.py
  13. 16  r2/r2/controllers/feedback.py
  14. 105  r2/r2/controllers/front.py
  15. 52  r2/r2/controllers/health.py
  16. 35  r2/r2/controllers/listingcontroller.py
  17. 13  r2/r2/controllers/post.py
  18. 420  r2/r2/controllers/promotecontroller.py
  19. 31  r2/r2/controllers/reddit_base.py
  20. 4  r2/r2/controllers/toolbar.py
  21. 386  r2/r2/controllers/validator/validator.py
  22. 1,470  r2/r2/i18n/r2.pot
  23. 211  r2/r2/lib/amqp.py
  24. 42  r2/r2/lib/app_globals.py
  25. 22  r2/r2/lib/authorize/__init__.py
  26. 589  r2/r2/lib/authorize/api.py
  27. 176  r2/r2/lib/authorize/interaction.py
  28. 2  r2/r2/lib/base.py
  29. 93  r2/r2/lib/contrib/nymph.py
  30. 13  r2/r2/lib/cssfilter.py
  31. 63  r2/r2/lib/db/queries.py
  32. 148  r2/r2/lib/db/query_queue.py
  33. 27  r2/r2/lib/db/sorts.py
  34. 14  r2/r2/lib/db/tdb_sql.py
  35. 24  r2/r2/lib/db/thing.py
  36. 200  r2/r2/lib/emailer.py
  37. 1  r2/r2/lib/filters.py
  38. 8  r2/r2/lib/jsonresponse.py
  39. 23  r2/r2/lib/jsontemplates.py
  40. 97  r2/r2/lib/media.py
  41. 47  r2/r2/lib/menus.py
  42. 102  r2/r2/lib/migrate.py
  43. 2  r2/r2/lib/normalized_hot.py
  44. 39  r2/r2/lib/organic.py
  45. 5  r2/r2/lib/pages/admin_pages.py
  46. 8  r2/r2/lib/pages/graph.py
  47. 745  r2/r2/lib/pages/pages.py
  48. 28  r2/r2/lib/pages/things.py
  49. 509  r2/r2/lib/promote.py
  50. 69  r2/r2/lib/services.py
  51. 93  r2/r2/lib/solrsearch.py
  52. 2  r2/r2/lib/spreadshirt.py
  53. 29  r2/r2/lib/strings.py
  54. 88  r2/r2/lib/template_helpers.py
  55. 14  r2/r2/lib/tracking.py
  56. 10  r2/r2/lib/traffic.py
  57. 10  r2/r2/lib/translation.py
  58. 72  r2/r2/lib/user_stats.py
  59. 105  r2/r2/lib/utils/utils.py
  60. 17  r2/r2/lib/workqueue.py
  61. 5  r2/r2/lib/wrapped.py
  62. 2  r2/r2/models/__init__.py
  63. 61  r2/r2/models/account.py
  64. 53  r2/r2/models/admintools.py
  65. 106  r2/r2/models/award.py
  66. 442  r2/r2/models/bidding.py
  67. 63  r2/r2/models/builder.py
  68. 141  r2/r2/models/link.py
  69. 67  r2/r2/models/mail_queue.py
  70. 4  r2/r2/models/populatedb.py
  71. 2  r2/r2/models/printable.py
  72. 55  r2/r2/models/subreddit.py
  73. 85  r2/r2/models/thing_changes.py
  74. 8  r2/r2/models/vote.py
  75. BIN  r2/r2/public/static/alien-clippy.png
  76. BIN  r2/r2/public/static/award.png
  77. BIN  r2/r2/public/static/bg-button-add.png
  78. BIN  r2/r2/public/static/bg-button-remove.png
  79. BIN  r2/r2/public/static/cclogo.png
  80. BIN  r2/r2/public/static/clippy-bullet.png
  81. 20  r2/r2/public/static/css/reddit-ie6-hax.css
  82. 8  r2/r2/public/static/css/reddit-ie7-hax.css
  83. 1,300  r2/r2/public/static/css/reddit.css
  84. 16  r2/r2/public/static/css/spreadshirt.css
  85. BIN  r2/r2/public/static/dragonage/bgfinal.jpg
  86. BIN  r2/r2/public/static/dragonage/topb.jpg
  87. BIN  r2/r2/public/static/gagged-alien.png
  88. BIN  r2/r2/public/static/gradient-button-hover.png
  89. BIN  r2/r2/public/static/gradient-button.png
  90. BIN  r2/r2/public/static/gradient-nub-hover.png
  91. BIN  r2/r2/public/static/gradient-nub.png
  92. 4,241  r2/r2/public/static/js/jquery-1.3.1.js
  93. 19  r2/r2/public/static/js/jquery-1.3.1.min.js
  94. 2  r2/r2/public/static/js/jquery.js
  95. 36  r2/r2/public/static/js/jquery.reddit.js
  96. 88  r2/r2/public/static/js/reddit.js
  97. 64  r2/r2/public/static/js/sponsored.js
  98. 519  r2/r2/public/static/js/ui.core.js
  99. 1,630  r2/r2/public/static/js/ui.datepicker.js
  100. BIN  r2/r2/public/static/reddit_ban.png
  101. BIN  r2/r2/public/static/reddit_edit.png
  102. BIN  r2/r2/public/static/reddit_reported.png
  103. BIN  r2/r2/public/static/reddit_spam.png
  104. BIN  r2/r2/public/static/reddit_traffic.png
  105. BIN  r2/r2/public/static/redditaddict/appsbar/barbg.png
  106. BIN  r2/r2/public/static/redditaddict/appsbar/buttonbg.png
  107. BIN  r2/r2/public/static/redditaddict/appsbar/nub.png
  108. BIN  r2/r2/public/static/redditaddict/badge/AIRInstallBadge.swf
  109. BIN  r2/r2/public/static/redditaddict/badge/badgeimage.jpg
  110. BIN  r2/r2/public/static/redditaddict/badge/expressinstall.swf
  111. 8  r2/r2/public/static/redditaddict/badge/swfobject.js
  112. BIN  r2/r2/public/static/redditaddict/images/arrows.png
  113. BIN  r2/r2/public/static/redditaddict/images/bg.png
  114. BIN  r2/r2/public/static/redditaddict/images/black50.png
  115. BIN  r2/r2/public/static/redditaddict/images/c-caps.png
  116. BIN  r2/r2/public/static/redditaddict/images/c-tile.png
  117. BIN  r2/r2/public/static/redditaddict/images/close.png
  118. BIN  r2/r2/public/static/redditaddict/images/reddit-head.png
  119. BIN  r2/r2/public/static/redditaddict/images/ss-graph.jpg
  120. BIN  r2/r2/public/static/redditaddict/images/ss-main.jpg
  121. BIN  r2/r2/public/static/redditaddict/images/star-blue.png
  122. BIN  r2/r2/public/static/redditaddict/images/star-orangered.png
  123. BIN  r2/r2/public/static/redditaddict/images/star-white.png
  124. BIN  r2/r2/public/static/redditaddict/images/support.png
  125. BIN  r2/r2/public/static/redditaddict/images/thumbsup3.png
  126. BIN  r2/r2/public/static/redditaddict/images/w-bot.png
  127. BIN  r2/r2/public/static/redditaddict/images/w-mid.png
  128. BIN  r2/r2/public/static/redditaddict/images/w-top.png
  129. 533  r2/r2/public/static/redditaddict/index.html
  130. BIN  r2/r2/public/static/socialite/appsbar/barbg.png
  131. BIN  r2/r2/public/static/socialite/appsbar/buttonbg.png
  132. BIN  r2/r2/public/static/socialite/appsbar/nub.png
  133. 442  r2/r2/public/static/socialite/index.html
  134. BIN  r2/r2/public/static/socialite/socialitelogo.png
  135. 84  r2/r2/templates/adminawardgive.html
  136. 96  r2/r2/templates/adminawards.html
  137. 69  r2/r2/templates/{userstats.html → adminawardwinners.html}
  138. 2  r2/r2/templates/admintranslations.html
  139. 34  r2/r2/templates/ads.html
  140. 35  r2/r2/templates/appservicemonitor.html
  141. 18  r2/r2/templates/base.htmllite
  142. 6  r2/r2/templates/clickgadget.html
  143. 7  r2/r2/templates/comment.html
  144. 8  r2/r2/templates/comment.htmllite
  145. 8  r2/r2/templates/comment.mobile
  146. 430  r2/r2/templates/createsubreddit.html
  147. 51  r2/r2/templates/dart_ad.html
  148. 3  r2/r2/templates/link.html
  149. 23  r2/r2/templates/link.htmllite
  150. 6  r2/r2/templates/link.mobile
  151. 2  r2/r2/templates/link.wired
  152. 49  r2/r2/templates/linkinfobar.html
  153. 27  r2/r2/templates/linkpromoteinfobar.html
  154. 17  r2/r2/templates/message.html
  155. 84  r2/r2/templates/messagecompose.html
  156. 3  r2/r2/templates/morechildren.html
  157. 209  r2/r2/templates/paymentform.html
  158. 5  r2/r2/templates/prefoptions.html
  159. 37  r2/r2/templates/prefupdate.html
  160. 53  r2/r2/templates/printable.html
  161. 75  r2/r2/templates/printablebuttons.html
  162. 140  r2/r2/templates/profilebar.html
  163. 118  r2/r2/templates/promo_email.email
  164. 276  r2/r2/templates/promote_graph.html
  165. 73  r2/r2/templates/promotedlink.html
  166. 83  r2/r2/templates/promotedlinks.html
  167. 58  r2/r2/templates/promotedtraffic.html
  168. 463  r2/r2/templates/promotelinkform.html
  169. 28  r2/r2/templates/reddit.html
  170. 2  r2/r2/templates/redditfooter.html
  171. 6  r2/r2/templates/redditheader.html
  172. 4  r2/r2/templates/reddittraffic.html
  173. 35  r2/r2/templates/selfserveblurb.html
  174. 5  r2/r2/templates/share.email
  175. 11  r2/r2/templates/shirtpane.html
  176. 8  r2/r2/templates/sidebox.html
  177. 35  r2/r2/templates/sidecontentbox.html
  178. 41  r2/r2/templates/sponsorshipbox.html
  179. 8  r2/r2/templates/subreddit.html
  180. 125  r2/r2/templates/subredditinfobar.html
  181. 24  r2/r2/templates/subredditstylesheet.html
  182. 1  r2/r2/templates/subscriptionbox.html
  183. 35  r2/r2/templates/takedownpane.html
  184. 6  r2/r2/templates/translatedstring.html
  185. 85  r2/r2/templates/trophycase.html
  186. 3  r2/r2/templates/uploadedimage.html
  187. 62  r2/r2/templates/userawards.html
  188. 31  r2/r2/templates/userlist.html
  189. 2  r2/r2/templates/usertableitem.html
  190. 10  r2/r2/templates/usertext.html
  191. 69  r2/r2/templates/utils.html
  192. 31  r2/r2/templates/verifyemail.email
  193. 66  r2/r2/templates/wrappeduser.html
  194. 11  r2/setup.py
  195. 8  r2/supervise_watcher.py
18  r2/Makefile
@@ -21,11 +21,14 @@
21 21
 ################################################################################
22 22
 
23 23
 # Jacascript files to be compressified
24  
-js_targets  = jquery.js jquery.json.js jquery.reddit.js reddit.js
  24
+js_targets  = jquery.js jquery.json.js jquery.reddit.js reddit.js ui.core.js ui.datepicker.js sponsored.js
25 25
 # CSS targets 
26  
-css_targets = reddit.css reddit-ie6-hax.css reddit-ie7-hax.css mobile.css spreadshirt.css
  26
+main_css = reddit.css
  27
+css_targets = reddit-ie6-hax.css reddit-ie7-hax.css mobile.css spreadshirt.css
27 28
 
28 29
 SED=sed
  30
+CAT=cat
  31
+CSS_COMPRESS = $(SED) -e 's/ \+/ /' -e 's/\/\*.*\*\///g' -e 's/: /:/' | grep -v "^ *$$"
29 32
 
30 33
 package    = r2
31 34
 static_dir = $(package)/public/static
@@ -41,7 +44,8 @@ PRIVATEREPOS = $(shell python -c 'exec "try: import r2admin; print r2admin.__pat
41 44
 
42 45
 JSTARGETS  := $(foreach js,  $(js_targets),  $(static_dir)/$(js))
43 46
 CSSTARGETS := $(foreach css, $(css_targets), $(static_dir)/$(css))
44  
-RTLCSS      = $(CSSTARGETS:.css=-rtl.css)
  47
+MAINCSS    := $(foreach css, $(main_css), $(static_dir)/$(css))
  48
+RTLCSS      = $(CSSTARGETS:.css=-rtl.css) $(MAINCSS:.css=-rtl.css)
45 49
 
46 50
 
47 51
 MD5S = $(JSTARGETS:=.md5) $(CSSTARGETS:=.md5)
@@ -67,10 +71,10 @@ $(JSTARGETS): 	$(static_dir)/%.js : $(static_dir)/js/%.js
67 71
 	$(JSCOMPRESS) < $< > $@
68 72
 
69 73
 $(CSSTARGETS): 	$(static_dir)/%.css : $(static_dir)/css/%.css
70  
-	$(SED) -e 's/ \+/ /'  \
71  
-	       -e 's/\/\*.*\*\///g' \
72  
-	       -e 's/: /:/' \
73  
-	   $< | grep -v "^ *$$" > $@
  74
+	$(CAT) $< | $(CSS_COMPRESS) > $@
  75
+
  76
+$(MAINCSS):  $(static_dir)/%.css : $(static_dir)/css/%.css
  77
+	python r2/lib/contrib/nymph.py $< | $(CSS_COMPRESS) > $@
74 78
 
75 79
 $(RTLCSS):	%-rtl.css : %.css
76 80
 	$(SED) -e "s/left/>####</g" \
87  r2/draw_load.py
... ...
@@ -0,0 +1,87 @@
  1
+from __future__ import with_statement
  2
+import Image, ImageDraw
  3
+
  4
+colors = [
  5
+    "#FFFFFF", "#f0f5FF", 
  6
+    "#E2ECFF", "#d6f5cb", 
  7
+    "#CAFF98", "#e4f484", 
  8
+    "#FFEA71", "#ffdb81", 
  9
+    "#FF9191", "#FF0000"]
  10
+
  11
+def get_load_level(host, nlevels = 8):
  12
+     # default number of cpus shall be 1
  13
+     ncpus = getattr(host, "ncpu", 1)
  14
+     # color code in nlevel levels
  15
+     return _load_int(host.load(), ncpus, nlevels = 8)
  16
+
  17
+def _load_int(current, max_val, nlevels = 8):
  18
+     i =  min(max(int(nlevels*current/max_val+0.4), 0),nlevels+1)
  19
+     return colors[i]
  20
+
  21
+def draw_load(row_size = 12, width = 200, out_file = "/tmp/load.png"):
  22
+    from r2.lib import services
  23
+    
  24
+    a = services.AppServiceMonitor()
  25
+    hosts = list(a)
  26
+    
  27
+    number = (len([x for x in hosts if x.services]) + 
  28
+              len([x for x in hosts if x.database]) +
  29
+              sum(len(x.queue.queues) for x in hosts if x.queue)) + 3
  30
+
  31
+    im = Image.new("RGB", (width, number * row_size + 2))
  32
+    draw = ImageDraw.Draw(im)
  33
+    def draw_box(label, color, center = False):
  34
+        ypos = draw_box.ypos
  35
+        xpos = 1
  36
+        if center:
  37
+            w, h = draw.textsize(label)
  38
+            xpos = (width - w) / 2
  39
+        draw.rectangle(((1, ypos), (width-2, ypos + row_size)), color)
  40
+        draw.text((xpos,ypos+1), label, fill = "#000000")
  41
+        draw_box.ypos += row_size
  42
+    draw_box.ypos = 0
  43
+
  44
+    draw_box(" ==== DATABASES ==== ", "#BBBBBB", center = True)
  45
+    for host in hosts:
  46
+        if host.database:
  47
+            draw_box("  %s load: %s" % (host.host, host.load()),
  48
+                     get_load_level(host))
  49
+
  50
+    draw_box(" ==== SERVICES ==== ", "#BBBBBB", center = True)
  51
+    for host in hosts:
  52
+        if host.services:
  53
+            draw_box("  %s load: %s" % (host.host, host.load()),
  54
+                     get_load_level(host))
  55
+
  56
+    draw_box(" ==== QUEUES ==== ", "#BBBBBB", center = True)
  57
+    for host in hosts:
  58
+        if host.queue:
  59
+            for name, data in host.queue:
  60
+                max_len = host.queue.max_length(name)
  61
+                draw_box(" %16s: %5s / %5s" % (name, data(), max_len),
  62
+                         _load_int(data(), max_len))
  63
+
  64
+    with open(out_file, 'w') as handle:
  65
+        im.save(handle, "PNG")
  66
+    
  67
+
  68
+def merge_images(out_file, file_names):
  69
+    images = []
  70
+    width = 0
  71
+    height = 0
  72
+    for f in file_names:
  73
+        images.append(Image.open(f))
  74
+        w, h = images[-1].size
  75
+        width = max(w, width)
  76
+        height += h
  77
+
  78
+    total = Image.new("RGB", (width, height))
  79
+    height = 0
  80
+    for im in images:
  81
+        total.paste(im, (0, height))
  82
+        w, h = im.size
  83
+        height += h
  84
+
  85
+    with open(out_file, 'w') as handle:
  86
+        total.save(out_file)
  87
+    
37  r2/example.ini
@@ -18,12 +18,33 @@ memcaches = 127.0.0.1:11211
18 18
 permacaches = 127.0.0.1:11211
19 19
 rendercaches = 127.0.0.1:11211
20 20
 rec_cache = 127.0.0.1:11311
  21
+
  22
+# site tracking urls.  All urls are assumed to be to an image unless
  23
+# otherwise noted:
21 24
 tracker_url = 
22 25
 adtracker_url = 
  26
+adframetracker_url = 
  27
+# for tracking clicks.  Should be the url of a redirector
23 28
 clicktracker_url = 
24 29
 traffic_url = 
25 30
 
26  
-databases = main, comment, vote, change, email, query_queue
  31
+# for sponsored links:
  32
+payment_domain = https://pay.localhost/
  33
+authorizenetname = 
  34
+authorizenetkey = 
  35
+authorizenetapi = 
  36
+min_promote_bid = 20
  37
+max_promote_bid = 9999
  38
+min_promote_future = 2
  39
+
  40
+
  41
+
  42
+amqp_host = localhost:5672
  43
+amqp_user = guest
  44
+amqp_pass = guest
  45
+amqp_virtual_host = /
  46
+
  47
+databases = main, comment, vote, change, email, authorize, award
27 48
 
28 49
 #db name         db           host       user, pass
29 50
 main_db =        newreddit,   127.0.0.1, ri,   password
@@ -32,7 +53,8 @@ comment2_db =    newreddit,   127.0.0.1, ri,   password
32 53
 vote_db =        newreddit,   127.0.0.1, ri,   password
33 54
 change_db =      changed,     127.0.0.1, ri,   password
34 55
 email_db =       email,       127.0.0.1, ri,   password
35  
-query_queue_db = query_queue, 127.0.0.1, ri,   password
  56
+authorize_db =   authorize,   127.0.0.1, ri,   password
  57
+award_db =       award,       127.0.0.1, ri,   password
36 58
 
37 59
 db_app_name = reddit
38 60
 db_create_tables = True
@@ -65,6 +87,10 @@ db_table_report_account_comment = relation, account, comment, comment
65 87
 db_table_report_account_message = relation, account, message, main
66 88
 db_table_report_account_subreddit = relation, account, subreddit, main
67 89
 
  90
+db_table_award = thing, award
  91
+db_table_trophy = relation, account, award, award
  92
+
  93
+disallow_db_writes = False
68 94
 
69 95
 ###
70 96
 # Other magic settings
@@ -88,16 +114,22 @@ allowed_css_linked_domains = my.domain.com, my.otherdomain.com
88 114
 css_killswitch = False
89 115
 max_sr_images = 20
90 116
 
  117
+show_awards = False
  118
+
91 119
 login_cookie = reddit_session
92 120
 domain = localhost
93 121
 domain_prefix = 
94 122
 media_domain = localhost
95 123
 default_sr = localhost
  124
+automatic_reddits = 
  125
+
96 126
 admins = 
97 127
 sponsors = 
  128
+paid_sponsors = 
98 129
 page_cache_time = 30
99 130
 static_path = /static/
100 131
 useragent = Mozilla/5.0 (compatible; bot/1.0; ChangeMe)
  132
+allow_shutdown = False
101 133
 
102 134
 solr_url =  
103 135
 solr_cache_time = 300
@@ -124,7 +156,6 @@ MODWINDOW = 2
124 156
 HOT_PAGE_AGE = 1
125 157
 
126 158
 #
127  
-media_period  = 10 minutes
128 159
 rising_period = 12 hours
129 160
 new_incubation = 90 seconds
130 161
 
6  r2/r2/config/middleware.py
@@ -60,6 +60,8 @@ def error_mapper(code, message, environ, global_conf=None, **kw):
60 60
             d['cnameframe'] = 1
61 61
         if environ.get('REDDIT_NAME'):
62 62
             d['srname'] = environ.get('REDDIT_NAME')
  63
+        if environ.get('REDDIT_TAKEDOWN'):
  64
+            d['takedown'] = environ.get('REDDIT_TAKEDOWN')
63 65
 
64 66
         #preserve x-sup-id when 304ing
65 67
         if code == 304:
@@ -288,7 +290,7 @@ def __call__(self, environ, start_response):
288 290
         sr_redirect = None
289 291
         for sd in list(sub_domains):
290 292
             # subdomains to disregard completely
291  
-            if sd in ('www', 'origin', 'beta'):
  293
+            if sd in ('www', 'origin', 'beta', 'pay'):
292 294
                 continue
293 295
             # subdomains which change the extension
294 296
             elif sd == 'm':
@@ -297,6 +299,7 @@ def __call__(self, environ, start_response):
297 299
                 environ['reddit-domain-extension'] = sd
298 300
             elif (len(sd) == 2 or (len(sd) == 5 and sd[2] == '-')) and self.lang_re.match(sd):
299 301
                 environ['reddit-prefer-lang'] = sd
  302
+                environ['reddit-domain-prefix'] = sd
300 303
             else:
301 304
                 sr_redirect = sd
302 305
                 sub_domains.remove(sd)
@@ -357,6 +360,7 @@ class ExtensionMiddleware(object):
357 360
                   'mobile' : ('mobile', 'text/html; charset=UTF-8'),
358 361
                   'png' : ('png', 'image/png'),
359 362
                   'css' : ('css', 'text/css'),
  363
+                  'csv' : ('csv', 'text/csv; charset=UTF-8'),
360 364
                   'api' : (api_type(), 'application/json; charset=UTF-8'),
361 365
                   'json' : (api_type(), 'application/json; charset=UTF-8'),
362 366
                   'json-html' : (api_type('html'), 'application/json; charset=UTF-8')}
35  r2/r2/config/routing.py
@@ -34,6 +34,7 @@ def make_map(global_conf={}, app_conf={}):
34 34
     
35 35
     mc('/login',    controller='front', action='login')
36 36
     mc('/logout',   controller='front', action='logout')
  37
+    mc('/verify',    controller='front', action='verify')
37 38
     mc('/adminon',  controller='front', action='adminon')
38 39
     mc('/adminoff', controller='front', action='adminoff')
39 40
     mc('/submit',   controller='front', action='submit')
@@ -51,6 +52,7 @@ def make_map(global_conf={}, app_conf={}):
51 52
     
52 53
     mc('/reddits/create', controller='front', action='newreddit')
53 54
     mc('/reddits/search', controller='front', action='search_reddits')
  55
+    mc('/reddits/login', controller='front', action='login')
54 56
     mc('/reddits/:where', controller='reddits', action='listing',
55 57
        where = 'popular',
56 58
        requirements=dict(where="popular|new|banned"))
@@ -69,8 +71,9 @@ def make_map(global_conf={}, app_conf={}):
69 71
     mc('/widget', controller='buttons', action='widget_demo_page')
70 72
     mc('/bookmarklets', controller='buttons', action='bookmarklets')
71 73
     
72  
-    mc('/stats', controller='front', action='stats')
  74
+    mc('/awards', controller='front', action='awards')
73 75
     
  76
+    mc('/i18n', controller='feedback', action='i18n')
74 77
     mc('/feedback', controller='feedback', action='feedback')
75 78
     mc('/ad_inq',   controller='feedback', action='ad_inq')
76 79
     
@@ -79,6 +82,10 @@ def make_map(global_conf={}, app_conf={}):
79 82
     mc('/admin/i18n/:action', controller='i18n')
80 83
     mc('/admin/i18n/:action/:lang', controller='i18n')
81 84
 
  85
+    mc('/admin/awards', controller='awards')
  86
+    mc('/admin/awards/:awardcn/:action', controller='awards',
  87
+       requirements=dict(action="give|winners"))
  88
+
82 89
     mc('/admin/:action', controller='admin')
83 90
     
84 91
     mc('/user/:username/about', controller='user', action='about',
@@ -115,8 +122,20 @@ def make_map(global_conf={}, app_conf={}):
115 122
     mc('/framebuster/:what/:blah',
116 123
        controller='front', action = 'framebuster')
117 124
 
118  
-    mc('/promote/edit_promo/:link', controller='promote', action = 'edit_promo')
119  
-    mc('/promote/:action', controller='promote')
  125
+    mc('/promoted/edit_promo/:link',
  126
+       controller='promote', action = 'edit_promo')
  127
+    mc('/promoted/pay/:link',
  128
+       controller='promote', action = 'pay')
  129
+    mc('/promoted/graph',
  130
+       controller='promote', action = 'graph')
  131
+    mc('/promoted/:action', controller='promote',
  132
+       requirements = dict(action = "new_promo"))
  133
+    mc('/promoted/:sort', controller='promote', action = "listing")
  134
+    mc('/promoted/', controller='promoted', action = "listing",
  135
+       sort = "")
  136
+
  137
+    mc('/health', controller='health', action='health')
  138
+    mc('/shutdown', controller='health', action='shutdown')
120 139
 
121 140
     mc('/', controller='hot', action='listing')
122 141
     
@@ -137,7 +156,7 @@ def make_map(global_conf={}, app_conf={}):
137 156
        requirements=dict(action="password|random|framebuster"))
138 157
     mc('/:action', controller='embed',
139 158
        requirements=dict(action="help|blog"))
140  
-    mc('/help/:anything', controller='embed', action='help')
  159
+    mc('/help/*anything', controller='embed', action='help')
141 160
     
142 161
     mc('/goto', controller='toolbar', action='goto')
143 162
     mc('/tb/:id', controller='toolbar', action='tb')
@@ -145,6 +164,8 @@ def make_map(global_conf={}, app_conf={}):
145 164
        requirements=dict(action="toolbar|inner|login"))
146 165
     mc('/toolbar/comments/:id', controller='toolbar', action='comments')
147 166
 
  167
+    mc('/c/:comment_id', controller='front', action='comment_by_id')
  168
+
148 169
     mc('/s/*rest', controller='toolbar', action='s')
149 170
     # additional toolbar-related rules just above the catchall
150 171
 
@@ -152,6 +173,8 @@ def make_map(global_conf={}, app_conf={}):
152 173
     
153 174
     mc('/resetpassword/:key', controller='front',
154 175
        action='resetpassword')
  176
+    mc('/verification/:key', controller='front',
  177
+       action='verify_email')
155 178
     mc('/resetpassword', controller='front',
156 179
        action='resetpassword')
157 180
 
@@ -165,6 +188,8 @@ def make_map(global_conf={}, app_conf={}):
165 188
        requirements=dict(action="login|register"))
166 189
     mc('/api/gadget/click/:ids', controller = 'api', action='gadget', type='click')
167 190
     mc('/api/gadget/:type', controller = 'api', action='gadget')
  191
+    mc('/api/:action', controller='promote',
  192
+       requirements=dict(action="promote|unpromote|new_promo|link_thumb|freebie|promote_note|update_pay|refund|traffic_viewer|rm_traffic_viewer"))
168 193
     mc('/api/:action', controller='api')
169 194
     
170 195
     mc('/captcha/:iden', controller='captcha', action='captchaimg')
@@ -184,6 +209,8 @@ def make_map(global_conf={}, app_conf={}):
184 209
 
185 210
     mc('/authorize_embed', controller = 'front', action = 'authorize_embed')
186 211
     
  212
+    mc("/ads/", controller = "front", action = "ad")
  213
+    mc("/ads/:reddit", controller = "front", action = "ad")
187 214
     # This route handles displaying the error page and 
188 215
     # graphics used in the 404/500
189 216
     # error pages. It should likely stay at the top 
2  r2/r2/config/templates.py
@@ -33,6 +33,7 @@ def api(type, cls):
33 33
 
34 34
 # class specific overrides
35 35
 api('link',          LinkJsonTemplate)
  36
+api('promotedlink',  PromotedLinkJsonTemplate)
36 37
 api('comment',       CommentJsonTemplate)
37 38
 api('message',       MessageJsonTemplate)
38 39
 api('subreddit',     SubredditJsonTemplate)
@@ -46,3 +47,4 @@ def api(type, cls):
46 47
 
47 48
 api('organiclisting',       OrganicListingJsonTemplate)
48 49
 api('reddittraffic', TrafficJsonTemplate)
  50
+api('takedownpane', TakedownJsonTemplate)
2  r2/r2/controllers/__init__.py
@@ -37,6 +37,7 @@
37 37
 
38 38
 from feedback import FeedbackController
39 39
 from front import FrontController
  40
+from health import HealthController
40 41
 from buttons import ButtonsController
41 42
 from captcha import CaptchaController
42 43
 from embed import EmbedController
@@ -44,6 +45,7 @@
44 45
 from post import PostController
45 46
 from toolbar import ToolbarController
46 47
 from i18n import I18nController
  48
+from awards import AwardsController
47 49
 from promotecontroller import PromoteController
48 50
 from mediaembed import MediaembedController
49 51
 
449  r2/r2/controllers/api.py
<
@@ -31,9 +31,10 @@
31 31
 import r2.models.thing_changes as tc
32 32
 
33 33
 from r2.lib.utils import get_title, sanitize_url, timeuntil, set_last_modified
34  
-from r2.lib.utils import query_string, to36, timefromnow, link_from_url
  34
+from r2.lib.utils import query_string, link_from_url, timefromnow, worker
  35
+from r2.lib.utils import timeago
35 36
 from r2.lib.pages import FriendList, ContributorList, ModList, \
36  
-    BannedList, BoringPage, FormPage, NewLink, CssError, UploadedImage, \
  37
+    BannedList, BoringPage, FormPage, CssError, UploadedImage, \
37 38
     ClickGadget
38 39
 from r2.lib.pages.things import wrap_links, default_thing_wrapper
39 40
 
@@ -44,31 +45,20 @@
44 45
 from r2.lib.strings import strings
45 46
 from r2.lib.filters import _force_unicode, websafe_json, websafe, spaceCompress
46 47
 from r2.lib.db import queries
  48
+from r2.lib import amqp, promote
47 49
 from r2.lib.media import force_thumbnail, thumbnail_url
48 50
 from r2.lib.comment_tree import add_comment, delete_comment
49 51
 from r2.lib import tracking, sup, cssfilter, emailer
50 52
 from r2.lib.subreddit_search import search_reddits
51 53
 
52  
-from simplejson import dumps
53  
-
54 54
 from datetime import datetime, timedelta
55 55
 from md5 import md5
56 56
 
57  
-from r2.lib.promote import promote, unpromote, get_promoted
58  
-
59 57
 class ApiController(RedditController):
60 58
     """
61 59
     Controller which deals with almost all AJAX site interaction.  
62 60
     """
63 61
 
64  
-    def response_func(self, kw):
65  
-        data = dumps(kw)
66  
-        if request.method == "GET" and request.GET.get("callback"):
67  
-            return "%s(%s)" % (websafe_json(request.GET.get("callback")),
68  
-                               websafe_json(data))
69  
-        return self.sendstring(data)
70  
-
71  
-
72 62
     @validatedForm()
73 63
     def ajax_login_redirect(self, form, jquery, dest):
74 64
         form.redirect("/login" + query_string(dict(dest=dest)))
@@ -89,7 +79,7 @@ def GET_info(self, link, count):
89 79
     @validatedForm(VCaptcha(),
90 80
                    name=VRequired('name', errors.NO_NAME),
91 81
                    email=ValidEmails('email', num = 1),
92  
-                   reason = VOneOf('reason', ('ad_inq', 'feedback')),
  82
+                   reason = VOneOf('reason', ('ad_inq', 'feedback', "i18n")),
93 83
                    message=VRequired('text', errors.NO_TEXT),
94 84
                    )
95 85
     def POST_feedback(self, form, jquery, name, email, reason, message):
@@ -98,14 +88,17 @@ def POST_feedback(self, form, jquery, name, email, reason, message):
98 88
                 form.has_errors('text', errors.NO_TEXT) or
99 89
                 form.has_errors('captcha', errors.BAD_CAPTCHA)):
100 90
 
101  
-            if reason != 'ad_inq':
102  
-                emailer.feedback_email(email, message, name, reply_to = '')
103  
-            else:
  91
+            if reason == 'ad_inq':
104 92
                 emailer.ad_inq_email(email, message, name, reply_to = '')
105  
-            
  93
+            elif reason == 'i18n':
  94
+                emailer.i18n_email(email, message, name, reply_to = '')
  95
+            else:
  96
+                emailer.feedback_email(email, message, name, reply_to = '')
106 97
             form.set_html(".status", _("thanks for your message! "
107 98
                             "you should hear back from us shortly."))
108 99
             form.set_inputs(text = "", captcha = "")
  100
+            form.find(".spacer").hide()
  101
+            form.find(".btn").hide()
109 102
 
110 103
     POST_ad_inq = POST_feedback
111 104
 
@@ -134,8 +127,6 @@ def POST_compose(self, form, jquery, to, subject, body, ip):
134 127
             if g.write_query_queue:
135 128
                 queries.new_message(m, inbox_rel)
136 129
 
137  
-
138  
-
139 130
     @validatedForm(VUser(),
140 131
                    VCaptcha(),
141 132
                    ValidDomain('url'),
@@ -148,7 +139,8 @@ def POST_compose(self, form, jquery, to, subject, body, ip):
148 139
                    save = VBoolean('save'),
149 140
                    selftext = VSelfText('text'),
150 141
                    kind = VOneOf('kind', ['link', 'self', 'poll']),
151  
-                   then = VOneOf('then', ('tb', 'comments'), default='comments'))
  142
+                   then = VOneOf('then', ('tb', 'comments'),
  143
+                                 default='comments'))
152 144
     def POST_submit(self, form, jquery, url, selftext, kind, title, save,
153 145
                     sr, ip, then):
154 146
         #backwards compatability
@@ -223,7 +215,7 @@ def POST_submit(self, form, jquery, url, selftext, kind, title, save,
223 215
 
224 216
         #set the ratelimiter
225 217
         if should_ratelimit:
226  
-            VRatelimit.ratelimit(rate_user=True, rate_ip = True, 
  218
+            VRatelimit.ratelimit(rate_user=True, rate_ip = True,
227 219
                                  prefix = "rate_submit_")
228 220
 
229 221
         #update the queries
@@ -231,6 +223,9 @@ def POST_submit(self, form, jquery, url, selftext, kind, title, save,
231 223
             queries.new_link(l)
232 224
             queries.new_vote(v)
233 225
 
  226
+        # also notifies the searchchanges
  227
+        worker.do(lambda: amqp.add_item('new_link', l._fullname))
  228
+
234 229
         #update the modified flags
235 230
         set_last_modified(c.user, 'overview')
236 231
         set_last_modified(c.user, 'submitted')
@@ -239,9 +234,6 @@ def POST_submit(self, form, jquery, url, selftext, kind, title, save,
239 234
         #update sup listings
240 235
         sup.add_update(c.user, 'submitted')
241 236
         
242  
-        # flag search indexer that something has changed
243  
-        tc.changed(l)
244  
-        
245 237
         if then == 'comments':
246 238
             path = add_sr(l.make_permalink_slow())
247 239
         elif then == 'tb':
@@ -250,7 +242,6 @@ def POST_submit(self, form, jquery, url, selftext, kind, title, save,
250 242
 
251 243
         form.redirect(path)
252 244
 
253  
-
254 245
     @validatedForm(VRatelimit(rate_ip = True,
255 246
                               rate_user = True,
256 247
                               prefix = 'fetchtitle_'),
@@ -286,7 +277,7 @@ def _login(self, form, user, dest='', rem = None):
286 277
     @validatedForm(VRatelimit(rate_ip = True, prefix = 'login_',
287 278
                               error = errors.WRONG_PASSWORD),
288 279
                    user = VLogin(['user', 'passwd']),
289  
-                   dest   = nop('dest'),
  280
+                   dest   = VDestination(),
290 281
                    rem    = VBoolean('rem'),
291 282
                    reason = VReason('reason'))
292 283
     def POST_login(self, form, jquery, user, dest, rem, reason):
@@ -303,7 +294,7 @@ def POST_login(self, form, jquery, user, dest, rem, reason):
303 294
                    name = VUname(['user']),
304 295
                    email = ValidEmails("email", num = 1),
305 296
                    password = VPassword(['passwd', 'passwd2']),
306  
-                   dest = nop('dest'),
  297
+                   dest = VDestination(),
307 298
                    rem = VBoolean('rem'),
308 299
                    reason = VReason('reason'))
309 300
     def POST_register(self, form, jquery, name, email,
@@ -347,7 +338,7 @@ def POST_register(self, form, jquery, name, email,
347 338
     @noresponse(VUser(),
348 339
                 VModhash(),
349 340
                 container = VByName('id'))
350  
-    def POST_leave_moderator(self, container):
  341
+    def POST_leavemoderator(self, container):
351 342
         """
352 343
         Handles self-removal as moderator from a subreddit as rendered
353 344
         in the subreddit sidebox on any of that subreddit's pages.
@@ -358,7 +349,7 @@ def POST_leave_moderator(self, container):
358 349
     @noresponse(VUser(),
359 350
                 VModhash(),
360 351
                 container = VByName('id'))
361  
-    def POST_leave_contributor(self, container):
  352
+    def POST_leavecontributor(self, container):
362 353
         """
363 354
         same comment as for POST_leave_moderator.
364 355
         """
@@ -371,7 +362,7 @@ def POST_leave_contributor(self, container):
371 362
                 nuser = VExistingUname('name'),
372 363
                 iuser = VByName('id'),
373 364
                 container = VByName('container'),
374  
-                type = VOneOf('type', ('friend', 'moderator',
  365
+                type = VOneOf('type', ('friend', 'moderator', 
375 366
                                        'contributor', 'banned')))
376 367
     def POST_unfriend(self, nuser, iuser, container, type):
377 368
         """
@@ -435,8 +426,9 @@ def POST_friend(self, form, jquery, ip, friend,
435 426
             form.set_html(".status:first", _("added"))
436 427
             if new and cls:
437 428
                 user_row = cls().user_row(friend)
438  
-                jquery("table").insert_table_rows(user_row)
439  
-                
  429
+                jquery("#" + type + "-table").show(
  430
+                    ).find("table").insert_table_rows(user_row)
  431
+
440 432
                 if type != 'friend':
441 433
                     msg = strings.msg_add_friend.get(type)
442 434
                     subj = strings.subj_add_friend.get(type)
@@ -447,14 +439,19 @@ def POST_friend(self, form, jquery, ip, friend,
447 439
                                  title = container.title)
448 440
                         msg = msg % d
449 441
                         subj = subj % d
450  
-                        Message._new(c.user, friend, subj, msg, ip)
  442
+                        item, inbox_rel = Message._new(c.user, friend,
  443
+                                                       subj, msg, ip)
  444
+
  445
+                        if g.write_query_queue:
  446
+                            queries.new_message(item, inbox_rel)
451 447
 
452 448
 
453 449
     @validatedForm(VUser('curpass', default = ''),
454  
-                   VModhash(), 
  450
+                   VModhash(),
455 451
                    email = ValidEmails("email", num = 1),
456  
-                   password = VPassword(['newpass', 'verpass']))
457  
-    def POST_update(self, form, jquery, email, password):
  452
+                   password = VPassword(['newpass', 'verpass']),
  453
+                   verify = VBoolean("verify"))
  454
+    def POST_update(self, form, jquery, email, password, verify):
458 455
         """
459 456
         handles /prefs/update for updating email address and password.
460 457
         """
@@ -467,11 +464,20 @@ def POST_update(self, form, jquery, email, password):
467 464
         # currently) apply it
468 465
         updated = False
469 466
         if (not form.has_errors("email", errors.BAD_EMAILS) and
470  
-            email and (not hasattr(c.user,'email') or c.user.email != email)):
471  
-            c.user.email = email
472  
-            c.user._commit()
473  
-            form.set_html('.status', _('your email has been updated'))
474  
-            updated = True
  467
+            email):
  468
+            if (not hasattr(c.user,'email') or c.user.email != email):
  469
+                c.user.email = email
  470
+                # unverified email for now
  471
+                c.user.email_verified = None
  472
+                c.user._commit()
  473
+                updated = True
  474
+            if verify:
  475
+                # TODO: rate limit this?
  476
+                emailer.verify_email(c.user, request.referer)
  477
+                form.set_html('.status',
  478
+                     _("you should be getting a verification email shortly."))
  479
+            else:
  480
+                form.set_html('.status', _('your email has been updated'))
475 481
             
476 482
         # change password
477 483
         if (password and
@@ -512,6 +518,8 @@ def POST_del(self, thing):
512 518
         if not thing: return
513 519
         '''for deleting all sorts of things'''
514 520
         thing._deleted = True
  521
+        if getattr(thing, "promoted", None) is not None:
  522
+            promote.delete_promo(thing)
515 523
         thing._commit()
516 524
 
517 525
         # flag search indexer that something has changed
@@ -535,9 +543,13 @@ def POST_del(self, thing):
535 543
                 thing = VByName('id'))
536 544
     def POST_report(self, thing):
537 545
         '''for reporting...'''
538  
-        if (thing and not thing._deleted and
539  
-            not (hasattr(thing, "promoted") and thing.promoted)):
540  
-            Report.new(c.user, thing)
  546
+        if not thing or thing._deleted:
  547
+            return
  548
+        elif c.user._spam or c.user.ignorereports:
  549
+            return
  550
+        elif getattr(thing, 'promoted', False):
  551
+            return
  552
+        Report.new(c.user, thing)
541 553
 
542 554
     @validatedForm(VUser(),
543 555
                    VModhash(),
@@ -555,7 +567,10 @@ def POST_editusertext(self, form, jquery, item, text):
555 567
                 kind = 'link'
556 568
                 item.selftext = text
557 569
 
558  
-            item.editted = True
  570
+            if (item._date < timeago('60 seconds')
  571
+                or (item._ups + item._downs > 2)):
  572
+                item.editted = True
  573
+
559 574
             item._commit()
560 575
 
561 576
             tc.changed(item)
@@ -591,7 +606,8 @@ def POST_comment(self, commentform, jquery, parent, comment, ip):
591 606
                 link = Link._byID(parent.link_id, data = True)
592 607
                 parent_comment = parent
593 608
             sr = parent.subreddit_slow
594  
-            if not sr.should_ratelimit(c.user, 'comment'):
  609
+            if ((link.is_self and link.author_id == c.user._id)
  610
+                or not sr.should_ratelimit(c.user, 'comment')):
595 611
                 should_ratelimit = False
596 612
 
597 613
         #remove the ratelimit error if the user's karma is high
@@ -616,12 +632,13 @@ def POST_comment(self, commentform, jquery, parent, comment, ip):
616 632
                                                comment, ip)
617 633
                 item.parent_id = parent._id
618 634
             else:
619  
-                item, inbox_rel =  Comment._new(c.user, link, parent_comment,
620  
-                                                comment, ip)
  635
+                item, inbox_rel = Comment._new(c.user, link, parent_comment,
  636
+                                               comment, ip)
621 637
                 Vote.vote(c.user, item, True, ip)
622  
-                # flag search indexer that something has changed
623  
-                tc.changed(item)
624  
-    
  638
+
  639
+                # will also update searchchanges as appropriate
  640
+                worker.do(lambda: amqp.add_item('new_comment', item._fullname))
  641
+
625 642
                 #update last modified
626 643
                 set_last_modified(c.user, 'overview')
627 644
                 set_last_modified(c.user, 'commented')
@@ -724,7 +741,7 @@ def POST_share(self, shareform, jquery, emails, thing, share_from, reply_to,
724 741
     def POST_vote(self, dir, thing, ip, vote_type):
725 742
         ip = request.ip
726 743
         user = c.user
727  
-        if not thing:
  744
+        if not thing or thing._deleted:
728 745
             return
729 746
 
730 747
         # TODO: temporary hack until we migrate the rest of the vote data
@@ -732,6 +749,8 @@ def POST_vote(self, dir, thing, ip, vote_type):
732 749
             g.log.debug("POST_vote: ignoring old vote on %s" % thing._fullname)
733 750
             return
734 751
 
  752
+        # in a lock to prevent duplicate votes from people
  753
+        # double-clicking the arrows
735 754
         with g.make_lock('vote_lock(%s,%s)' % (c.user._id36, thing._id36)):
736 755
             dir = (True if dir > 0
737 756
                    else False if dir < 0
@@ -846,26 +865,30 @@ def POST_delete_sr_img(self, form, jquery, name):
846 865
             return self.abort(403,'forbidden')
847 866
         c.site.del_image(name)
848 867
         c.site._commit()
849  
-    
  868
+
850 869
 
851 870
     @validatedForm(VSrModerator(),
852  
-                   VModhash())
853  
-    def POST_delete_sr_header(self, form, jquery):
  871
+                   VModhash(),
  872
+                   sponsor = VInt("sponsor", min = 0, max = 1))
  873
+    def POST_delete_sr_header(self, form, jquery, sponsor):
854 874
         """
855 875
         Called when the user request that the header on a sr be reset.
856 876
         """
857 877
         # just in case we need to kill this feature from XSS
858 878
         if g.css_killswitch:
859 879
             return self.abort(403,'forbidden')
860  
-        if c.site.header:
  880
+        if sponsor and c.user_is_admin:
  881
+            c.site.sponsorship_img = None
  882
+            c.site._commit()
  883
+        elif c.site.header:
  884
+            # reset the header image on the page
  885
+            jquery('#header-img').attr("src", DefaultSR.header)
861 886
             c.site.header = None
862 887
             c.site._commit()
863  
-        # reset the header image on the page
864  
-        form.find('#header-img').attr("src", DefaultSR.header)
865 888
         # hide the button which started this
866  
-        form.find('#delete-img').hide()
  889
+        form.find('.delete-img').hide()
867 890
         # hide the preview box
868  
-        form.find('#img-preview-container').hide()
  891
+        form.find('.img-preview-container').hide()
869 892
         # reset the status boxes
870 893
         form.set_html('.img-status', _("deleted"))
871 894
         
@@ -885,8 +908,10 @@ def GET_upload_sr_img(self, *a, **kw):
885 908
               VModhash(),
886 909
               file = VLength('file', max_length=1024*500),
887 910
               name = VCssName("name"),
888  
-              header = nop('header'))
889  
-    def POST_upload_sr_img(self, file, header, name):
  911
+              form_id = VLength('formid', max_length = 100), 
  912
+              header = VInt('header', max=1, min=0),
  913
+              sponsor = VInt('sponsor', max=1, min=0))
  914
+    def POST_upload_sr_img(self, file, header, sponsor, name, form_id):
890 915
         """
891 916
         Called on /about/stylesheet when an image needs to be replaced
892 917
         or uploaded, as well as on /about/edit for updating the
@@ -909,13 +934,16 @@ def POST_upload_sr_img(self, file, header, name):
909 934
         try:
910 935
             cleaned = cssfilter.clean_image(file,'PNG')
911 936
             if header:
912  
-                num = None # there is one and only header, and it is unnumbered
  937
+                # there is one and only header, and it is unnumbered
  938
+                resource = None 
  939
+            elif sponsor and c.user_is_admin:
  940
+                resource = "sponsor"
913 941
             elif not name:
914 942
                 # error if the name wasn't specified or didn't satisfy
915 943
                 # the validator
916 944
                 errors['BAD_CSS_NAME'] = _("bad image name")
917 945
             else:
918  
-                num = c.site.add_image(name, max_num = g.max_sr_images)
  946
+                resource = c.site.add_image(name, max_num = g.max_sr_images)
919 947
                 c.site._commit()
920 948
 
921 949
         except cssfilter.BadImage:
@@ -931,15 +959,18 @@ def POST_upload_sr_img(self, file, header, name):
931 959
         else: 
932 960
             # with the image num, save the image an upload to s3.  the
933 961
             # header image will be of the form "${c.site._fullname}.png"
934  
-            # while any other image will be ${c.site._fullname}_${num}.png
935  
-            new_url = cssfilter.save_sr_image(c.site, cleaned, num = num)
  962
+            # while any other image will be ${c.site._fullname}_${resource}.png
  963
+            new_url = cssfilter.save_sr_image(c.site, cleaned,
  964
+                                              resource = resource)
936 965
             if header:
937 966
                 c.site.header = new_url
  967
+            elif sponsor and c.user_is_admin:
  968
+                c.site.sponsorship_img = new_url
938 969
             c.site._commit()
939  
-    
  970
+
940 971
             return UploadedImage(_('saved'), new_url, name, 
941  
-                                 errors = errors).render()
942  
-    
  972
+                                 errors = errors, form_id = form_id).render()
  973
+
943 974
 
944 975
     @validatedForm(VUser(),
945 976
                    VModhash(),
@@ -950,21 +981,27 @@ def POST_upload_sr_img(self, file, header, name):
950 981
                    name = VSubredditName("name"),
951 982
                    title = VLength("title", max_length = 100),
952 983
                    domain = VCnameDomain("domain"),
953  
-                   description = VLength("description", max_length = 500),
  984
+                   description = VLength("description", max_length = 1000),
954 985
                    lang = VLang("lang"),
955 986
                    over_18 = VBoolean('over_18'),
956 987
                    show_media = VBoolean('show_media'),
957 988
                    type = VOneOf('type', ('public', 'private', 'restricted')),
958 989
                    ip = ValidIP(),
  990
+                   ad_type = VOneOf('ad', ('default', 'basic', 'custom')),
  991
+                   ad_file = VLength('ad-location', max_length = 500),
  992
+                   sponsor_name =VLength('sponsorship-name', max_length = 500),
  993
+                   sponsor_url = VLength('sponsorship-url', max_length = 500),
  994
+                   css_on_cname = VBoolean("css_on_cname"),
959 995
                    )
960  
-    def POST_site_admin(self, form, jquery, name ='', ip = None, sr = None, **kw):
  996
+    def POST_site_admin(self, form, jquery, name, ip, sr, ad_type, ad_file,
  997
+                        sponsor_url, sponsor_name,  **kw):
961 998
         # the status button is outside the form -- have to reset by hand
962 999
         form.parent().set_html('.status', "")
963 1000
 
964 1001
         redir = False
965 1002
         kw = dict((k, v) for k, v in kw.iteritems()
966 1003
                   if k in ('name', 'title', 'domain', 'description', 'over_18',
967  
-                           'show_media', 'type', 'lang',))
  1004
+                           'show_media', 'type', 'lang', "css_on_cname"))
968 1005
 
969 1006
         #if a user is banned, return rate-limit errors
970 1007
         if c.user._spam:
@@ -976,11 +1013,8 @@ def POST_site_admin(self, form, jquery, name ='', ip = None, sr = None, **kw):
976 1013
         if cname_sr and (not sr or sr != cname_sr):
977 1014
             c.errors.add(errors.USED_CNAME)
978 1015
 
979  
-        if not sr and form.has_errors(None, errors.RATELIMIT):
980  
-            # this form is a little odd in that the error field
981  
-            # doesn't occur within the form, so we need to manually
982  
-            # set this text
983  
-            form.parent().find('.RATELIMIT').html(c.errors[errors.RATELIMIT].message).show()
  1016
+        if not sr and form.has_errors("ratelimit", errors.RATELIMIT):
  1017
+            pass
984 1018
         elif not sr and form.has_errors("name", errors.SUBREDDIT_EXISTS,
985 1019
                                         errors.BAD_SR_NAME):
986 1020
             form.find('#example_name').hide()
@@ -996,13 +1030,17 @@ def POST_site_admin(self, form, jquery, name ='', ip = None, sr = None, **kw):
996 1030
             #sending kw is ok because it was sanitized above
997 1031
             sr = Subreddit._new(name = name, author_id = c.user._id, ip = ip,
998 1032
                                 **kw)
  1033
+
  1034
+            # will also update search
  1035
+            worker.do(lambda: amqp.add_item('new_subreddit', sr._fullname))
  1036
+
999 1037
             Subreddit.subscribe_defaults(c.user)
1000 1038
             # make sure this user is on the admin list of that site!
1001 1039
             if sr.add_subscriber(c.user):
1002 1040
                 sr._incr('_ups', 1)
1003 1041
             sr.add_moderator(c.user)
1004 1042
             sr.add_contributor(c.user)
1005  
-            redir =  sr.path + "about/edit/?created=true"
  1043
+            redir = sr.path + "about/edit/?created=true"
1006 1044
             if not c.user_is_admin:
1007 1045
                 VRatelimit.ratelimit(rate_user=True,
1008 1046
                                      rate_ip = True,
@@ -1010,9 +1048,20 @@ def POST_site_admin(self, form, jquery, name ='', ip = None, sr = None, **kw):
1010 1048
 
1011 1049
         #editting an existing reddit
1012 1050
         elif sr.is_moderator(c.user) or c.user_is_admin:
  1051
+
  1052
+            if c.user_is_admin:
  1053
+                sr.ad_type = ad_type
  1054
+                if ad_type != "custom":
  1055
+                    ad_file = Subreddit._defaults['ad_file']
  1056
+                sr.ad_file = ad_file
  1057
+                sr.sponsorship_url = sponsor_url or None
  1058
+                sr.sponsorship_name = sponsor_name or None
  1059
+
1013 1060
             #assume sr existed, or was just built
1014 1061
             old_domain = sr.domain
1015 1062
 
  1063
+            if not sr.domain:
  1064
+                del kw['css_on_cname']
1016 1065
             for k, v in kw.iteritems():
1017 1066
                 setattr(sr, k, v)
1018 1067
             sr._commit()
@@ -1028,6 +1077,8 @@ def POST_site_admin(self, form, jquery, name ='', ip = None, sr = None, **kw):
1028 1077
 
1029 1078
         if redir:
1030 1079
             form.redirect(redir)
  1080
+        else:
  1081
+            jquery.refresh()
1031 1082
 
1032 1083
     @noresponse(VUser(), VModhash(),
1033 1084
                 VSrCanBan('id'),
@@ -1108,7 +1159,7 @@ def POST_morechildren(self, form, jquery,
1108 1159
         user = c.user if c.user_is_loggedin else None
1109 1160
         if not link or not link.subreddit_slow.can_view(user):
1110 1161
             return self.abort(403,'forbidden')
1111  
-            
  1162
+
1112 1163
         if children:
1113 1164
             builder = CommentBuilder(link, CommentSortMenu.operator(sort),
1114 1165
                                      children)
@@ -1124,7 +1175,7 @@ def _children(cur_items):
1124 1175
                             cm.child = None
1125 1176
                         else:
1126 1177
                             items.append(cm.child)
1127  
-                        
  1178
+
1128 1179
                 return items
1129 1180
             # assumes there is at least one child
1130 1181
             # a = _children(items[0].child.things)
@@ -1191,10 +1242,11 @@ def POST_password(self, form, jquery, user):
1191 1242
             return
1192 1243
         else:
1193 1244
             emailer.password_email(user)
1194  
-            form.set_html(".status", _("an email will be sent to that account's address shortly"))
  1245
+            form.set_html(".status",
  1246
+                          _("an email will be sent to that account's address shortly"))
1195 1247
 
1196 1248
             
1197  
-    @validatedForm(cache_evt = VCacheKey('reset', ('key', 'name')),
  1249
+    @validatedForm(cache_evt = VCacheKey('reset', ('key',)),
1198 1250
                    password  = VPassword(['passwd', 'passwd2']))
1199 1251
     def POST_resetpassword(self, form, jquery, cache_evt, password):
1200 1252
         if form.has_errors('name', errors.EXPIRED):
@@ -1280,6 +1332,91 @@ def POST_enable_lang(self, tr):
1280 1332
             tr._is_enabled = True
1281 1333
 
1282 1334
 
  1335
+    @validatedForm(VAdmin(),
  1336
+                   award = VByName("fullname"),
  1337
+                   colliding_award=VAwardByCodename(("codename", "fullname")),
  1338
+                   codename = VLength("codename", max_length = 100),
  1339
+                   title = VLength("title", max_length = 100),
  1340
+                   imgurl = VLength("imgurl", max_length = 1000))
  1341
+    def POST_editaward(self, form, jquery, award, colliding_award, codename,
  1342
+                       title, imgurl):
  1343
+        if form.has_errors(("codename", "title", "imgurl"), errors.NO_TEXT):
  1344
+            pass
  1345
+
  1346
+        if form.has_errors(("codename"), errors.INVALID_OPTION):
  1347
+            form.set_html(".status", "some other award has that codename")
  1348
+            pass
  1349
+
  1350
+        if form.has_error():
  1351
+            return
  1352
+
  1353
+        if award is None:
  1354
+            Award._new(codename, title, imgurl)
  1355
+            form.set_html(".status", "saved. reload to see it.")
  1356
+            return
  1357
+
  1358
+        award.codename = codename
  1359
+        award.title = title
  1360
+        award.imgurl = imgurl
  1361
+        award._commit()
  1362
+        form.set_html(".status", _('saved'))
  1363
+
  1364
+    @validatedForm(VAdmin(),
  1365
+                   award = VByName("fullname"),
  1366
+                   description = VLength("description", max_length=1000),
  1367
+                   url = VLength("url", max_length=1000),
  1368
+                   cup_hours = VFloat("cup_hours",
  1369
+                                      coerce=False, min=0, max=24 * 365),
  1370
+                   recipient = VExistingUname("recipient"))
  1371
+    def POST_givetrophy(self, form, jquery, award, description,
  1372
+                        url, cup_hours, recipient):
  1373
+        if form.has_errors("award", errors.NO_TEXT):
  1374
+            pass
  1375
+
  1376
+        if form.has_errors("recipient", errors.USER_DOESNT_EXIST):
  1377
+            pass
  1378
+
  1379
+        if form.has_errors("recipient", errors.NO_USER):
  1380
+            pass
  1381
+
  1382
+        if form.has_errors("fullname", errors.NO_TEXT):
  1383
+            pass
  1384
+
  1385
+        if form.has_errors("cup_hours", errors.BAD_NUMBER):
  1386
+            pass
  1387
+
  1388
+        if form.has_error():
  1389
+            return
  1390
+
  1391
+        if cup_hours:
  1392
+            cup_seconds = int(cup_hours * 3600)
  1393
+            cup_expiration = timefromnow("%s seconds" % cup_seconds)
  1394
+        else:
  1395
+            cup_expiration = None
  1396
+
  1397
+        t = Trophy._new(recipient, award, description=description,
  1398
+                        url=url, cup_expiration=cup_expiration)
  1399
+
  1400
+        form.set_html(".status", _('saved'))
  1401
+
  1402
+    @validatedForm(VAdmin(),
  1403
+                   account = VExistingUname("account"))
  1404
+    def POST_removecup(self, form, jquery, account):
  1405
+        if not account:
  1406
+            return self.abort404()
  1407
+        account.remove_cup()
  1408
+
  1409
+    @validatedForm(VAdmin(),
  1410
+                   trophy = VTrophy("trophy_fn"))
  1411
+    def POST_removetrophy(self, form, jquery, trophy):
  1412
+        if not trophy:
  1413
+            return self.abort404()
  1414
+        recipient = trophy._thing1
  1415
+        award = trophy._thing2
  1416
+        trophy._delete()
  1417
+        Trophy.by_account(recipient, _update=True)
  1418
+        Trophy.by_award(award, _update=True)
  1419
+
1283 1420
     @validatedForm(links = VByName('links', thing_cls = Link, multiple = True),
1284 1421
                    show = VByName('show', thing_cls = Link, multiple = False))
1285 1422
     def POST_fetch_links(self, form, jquery, links, show):
@@ -1300,113 +1437,6 @@ def POST_disable_ui(self, ui_elem):
1300 1437
                 setattr(c.user, "pref_" + ui_elem, False)
1301 1438
                 c.user._commit()
1302 1439
 
1303  
-    @noresponse(VSponsor(),
1304  
-                thing = VByName('id'))
1305  
-    def POST_unpromote(self, thing):
1306  
-        if not thing: return
1307  
-        unpromote(thing)
1308  
-
1309  
-    @validatedForm(VSponsor(),
1310  
-                   ValidDomain('url'),
1311  
-                   ip               = ValidIP(),
1312  
-                   l                = VLink('link_id'),
1313  
-                   title            = VTitle('title'),
1314  
-                   url              = VUrl(['url', 'sr'], allow_self = False),
1315  
-                   sr               = VSubmitSR('sr'),
1316  
-                   subscribers_only = VBoolean('subscribers_only'),
1317  
-                   disable_comments = VBoolean('disable_comments'),
1318  
-                   expire           = VOneOf('expire', ['nomodify', 
1319  
-                                                        'expirein', 'cancel']),
1320  
-                   timelimitlength  = VInt('timelimitlength',1,1000),
1321