mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-04-27 03:00:12 -04:00
1143 lines
42 KiB
Python
Executable File
1143 lines
42 KiB
Python
Executable File
# 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 reddit Inc.
|
|
#
|
|
# All portions of the code written by reddit are Copyright (c) 2006-2013 reddit
|
|
# Inc. All Rights Reserved.
|
|
###############################################################################
|
|
|
|
from oauth2 import OAuth2ResourceController, require_oauth2_scope
|
|
from reddit_base import RedditController, base_listing
|
|
|
|
from r2.models import *
|
|
from r2.models.query_cache import CachedQuery, MergedCachedQuery
|
|
from r2.config.extensions import is_api
|
|
from r2.lib.pages import *
|
|
from r2.lib.pages.things import wrap_links
|
|
from r2.lib.menus import TimeMenu, SortMenu, RecSortMenu, ProfileSortMenu
|
|
from r2.lib.menus import ControversyTimeMenu
|
|
from r2.lib.rising import get_rising
|
|
from r2.lib.wrapped import Wrapped
|
|
from r2.lib.normalized_hot import normalized_hot, get_hot
|
|
from r2.lib.db.thing import Query, Merge, Relations
|
|
from r2.lib.db import queries
|
|
from r2.lib.strings import Score
|
|
from r2.lib import organic
|
|
import r2.lib.search as search
|
|
from r2.lib.template_helpers import add_sr
|
|
from r2.lib.utils import iters, check_cheating, timeago
|
|
from r2.lib import sup
|
|
from r2.lib.validator import *
|
|
from r2.lib.butler import extract_user_mentions
|
|
import socket
|
|
|
|
from api_docs import api_doc, api_section
|
|
|
|
from pylons.i18n import _
|
|
from pylons.controllers.util import redirect_to
|
|
|
|
import random
|
|
from functools import partial
|
|
|
|
class ListingController(RedditController, OAuth2ResourceController):
|
|
"""Generalized controller for pages with lists of links."""
|
|
|
|
# toggle skipping of links based on the users' save/hide/vote preferences
|
|
skip = True
|
|
|
|
# allow stylesheets on listings
|
|
allow_stylesheets = True
|
|
|
|
# toggles showing numbers
|
|
show_nums = True
|
|
|
|
# any text that should be shown on the top of the page
|
|
infotext = None
|
|
|
|
# builder class to use to generate the listing. if none, we'll try
|
|
# to figure it out based on the query type
|
|
builder_cls = None
|
|
|
|
# page title
|
|
title_text = ''
|
|
|
|
# login box, subreddit box, submit box, etc, visible
|
|
show_sidebar = True
|
|
show_chooser = False
|
|
|
|
# class (probably a subclass of Reddit) to use to render the page.
|
|
render_cls = Reddit
|
|
|
|
# class for suggestions next to "next/prev" buttons
|
|
next_suggestions_cls = None
|
|
|
|
#extra parameters to send to the render_cls constructor
|
|
render_params = {}
|
|
extra_page_classes = ['listing-page']
|
|
|
|
def pre(self):
|
|
self.check_for_bearer_token()
|
|
RedditController.pre(self)
|
|
|
|
@property
|
|
def menus(self):
|
|
"""list of menus underneat the header (e.g., sort, time, kind,
|
|
etc) to be displayed on this listing page"""
|
|
return []
|
|
|
|
def build_listing(self, num, after, reverse, count, **kwargs):
|
|
"""uses the query() method to define the contents of the
|
|
listing and renders the page self.render_cls(..).render() with
|
|
the listing as contents"""
|
|
self.num = num
|
|
self.count = count
|
|
self.after = after
|
|
self.reverse = reverse
|
|
|
|
self.query_obj = self.query()
|
|
self.builder_obj = self.builder()
|
|
self.listing_obj = self.listing()
|
|
|
|
content = self.content()
|
|
return self.render_cls(content=content,
|
|
page_classes=self.extra_page_classes,
|
|
show_sidebar=self.show_sidebar,
|
|
show_chooser=self.show_chooser,
|
|
nav_menus=self.menus,
|
|
title=self.title(),
|
|
robots=getattr(self, "robots", None),
|
|
**self.render_params).render()
|
|
|
|
def content(self):
|
|
"""Renderable object which will end up as content of the render_cls"""
|
|
return self.listing_obj
|
|
|
|
def query(self):
|
|
"""Query to execute to generate the listing"""
|
|
raise NotImplementedError
|
|
|
|
def builder(self):
|
|
#store the query itself so it can be used elsewhere
|
|
if self.builder_cls:
|
|
builder_cls = self.builder_cls
|
|
elif isinstance(self.query_obj, Query):
|
|
builder_cls = QueryBuilder
|
|
elif isinstance(self.query_obj, search.SearchQuery):
|
|
builder_cls = SearchBuilder
|
|
elif isinstance(self.query_obj, iters):
|
|
builder_cls = IDBuilder
|
|
elif isinstance(self.query_obj, (queries.CachedResults, queries.MergedCachedResults)):
|
|
builder_cls = IDBuilder
|
|
elif isinstance(self.query_obj, (CachedQuery, MergedCachedQuery)):
|
|
builder_cls = IDBuilder
|
|
|
|
b = builder_cls(self.query_obj,
|
|
num = self.num,
|
|
skip = self.skip,
|
|
after = self.after,
|
|
count = self.count,
|
|
reverse = self.reverse,
|
|
keep_fn = self.keep_fn(),
|
|
wrap = self.builder_wrapper)
|
|
|
|
return b
|
|
|
|
def keep_fn(self):
|
|
def keep(item):
|
|
wouldkeep = item.keep_item(item)
|
|
if getattr(item, "promoted", None) is not None:
|
|
return False
|
|
if item._deleted and not c.user_is_admin:
|
|
return False
|
|
return wouldkeep
|
|
return keep
|
|
|
|
def listing(self):
|
|
"""Listing to generate from the builder"""
|
|
if (getattr(c.site, "_id", -1) == get_promote_srid() and
|
|
not c.user_is_sponsor):
|
|
abort(403, 'forbidden')
|
|
model = LinkListing(self.builder_obj, show_nums=self.show_nums)
|
|
suggestions = None
|
|
if self.next_suggestions_cls:
|
|
suggestions = self.next_suggestions_cls()
|
|
pane = model.listing(next_suggestions=suggestions)
|
|
# Indicate that the comment tree wasn't built for comments
|
|
for i in pane:
|
|
if hasattr(i, 'full_comment_path'):
|
|
i.child = None
|
|
return pane
|
|
|
|
def title(self):
|
|
"""Page <title>"""
|
|
return _(self.title_text) + " : " + c.site.name
|
|
|
|
def rightbox(self):
|
|
"""Contents of the right box when rendering"""
|
|
pass
|
|
|
|
builder_wrapper = staticmethod(default_thing_wrapper())
|
|
|
|
@require_oauth2_scope("read")
|
|
@base_listing
|
|
def GET_listing(self, **env):
|
|
check_cheating('site')
|
|
return self.build_listing(**env)
|
|
|
|
listing_api_doc = partial(
|
|
api_doc,
|
|
section=api_section.listings,
|
|
extends=ListingController.GET_listing,
|
|
extensions=["json", "xml"],
|
|
)
|
|
|
|
class FixListing(object):
|
|
"""When sorting by hotness, computing a listing when the before/after
|
|
link has a hottness of 0 is very slow. This class avoids drawing
|
|
next/prev links when that will happen."""
|
|
fix_listing = True
|
|
|
|
def listing(self):
|
|
listing = ListingController.listing(self)
|
|
|
|
if not self.fix_listing:
|
|
return listing
|
|
|
|
#404 existing bad pages
|
|
if self.after and self.after._hot == 0:
|
|
self.abort404()
|
|
|
|
#don't draw next/prev links for
|
|
if listing.things:
|
|
if listing.things[-1]._hot == 0:
|
|
listing.next = None
|
|
|
|
if listing.things[0]._hot == 0:
|
|
listing.prev = None
|
|
|
|
return listing
|
|
|
|
class HotController(FixListing, ListingController):
|
|
where = 'hot'
|
|
extra_page_classes = ListingController.extra_page_classes + ['hot-page']
|
|
show_chooser = True
|
|
next_suggestions_cls = ListingSuggestions
|
|
|
|
def make_requested_ad(self):
|
|
try:
|
|
link = Link._by_fullname(self.requested_ad, data=True)
|
|
except NotFound:
|
|
self.abort404()
|
|
|
|
if not (link.promoted and
|
|
(c.user_is_sponsor or
|
|
c.user_is_loggedin and link.author_id == c.user._id)):
|
|
self.abort403()
|
|
|
|
if not promote.is_live_on_sr(link, c.site.name):
|
|
self.abort403()
|
|
|
|
res = wrap_links([link._fullname], wrapper=self.builder_wrapper,
|
|
skip=False)
|
|
res.parent_name = "promoted"
|
|
if res.things:
|
|
return res
|
|
|
|
def make_single_ad(self):
|
|
srs = promote.srs_with_live_promos(c.user, c.site)
|
|
if srs:
|
|
srnames = [sr.name for sr in srs]
|
|
return SpotlightListing(show_promo=True, srnames=srnames,
|
|
navigable=False).listing()
|
|
|
|
def make_spotlight(self):
|
|
"""Build the Spotlight.
|
|
|
|
The frontpage gets a Spotlight box that contains promoted and organic
|
|
links from the user's subscribed subreddits and promoted links targeted
|
|
to the frontpage. If the user has disabled ads promoted links will not
|
|
be shown. Promoted links are requested from the adserver client-side.
|
|
|
|
"""
|
|
|
|
organic_fullnames = organic.organic_links(c.user)
|
|
promoted_links = []
|
|
|
|
# If prefs allow it, mix in promoted links and sr discovery content
|
|
if ((c.user.pref_show_sponsors or not c.user.gold)
|
|
and g.live_config['sr_discovery_links']):
|
|
organic_fullnames.extend(g.live_config['sr_discovery_links'])
|
|
|
|
show_promo = False
|
|
srnames = []
|
|
if c.user.pref_show_sponsors or not c.user.gold:
|
|
srs = promote.srs_with_live_promos(c.user, c.site)
|
|
if srs:
|
|
if ((c.user_is_loggedin and random.random() > 0.5) or
|
|
not c.user_is_loggedin):
|
|
srnames = [sr.name for sr in srs]
|
|
show_promo = True
|
|
|
|
random.shuffle(organic_fullnames)
|
|
organic_fullnames = organic_fullnames[:10]
|
|
b = IDBuilder(organic_fullnames,
|
|
wrap=self.builder_wrapper,
|
|
keep_fn=organic.keep_fresh_links,
|
|
skip=True)
|
|
organic_links = b.get_items()[0]
|
|
|
|
has_subscribed = c.user.has_subscribed
|
|
interestbar_prob = g.live_config['spotlight_interest_sub_p'
|
|
if has_subscribed else
|
|
'spotlight_interest_nosub_p']
|
|
interestbar = InterestBar(has_subscribed)
|
|
|
|
s = SpotlightListing(organic_links=organic_links,
|
|
interestbar=interestbar,
|
|
interestbar_prob=interestbar_prob,
|
|
show_promo=show_promo,
|
|
srnames=srnames,
|
|
max_num = self.listing_obj.max_num,
|
|
max_score = self.listing_obj.max_score).listing()
|
|
return s
|
|
|
|
def query(self):
|
|
#no need to worry when working from the cache
|
|
# TODO: just remove this then since we're always using the query cache
|
|
self.fix_listing = False
|
|
|
|
if isinstance(c.site, DefaultSR):
|
|
if c.user_is_loggedin:
|
|
srlimit = Subreddit.DEFAULT_LIMIT
|
|
over18 = c.user.has_subscribed and c.over18
|
|
else:
|
|
srlimit = g.num_default_reddits
|
|
over18 = False
|
|
|
|
sr_ids = Subreddit.user_subreddits(c.user,
|
|
limit=srlimit,
|
|
over18=over18)
|
|
return normalized_hot(sr_ids)
|
|
|
|
elif isinstance(c.site, MultiReddit):
|
|
return normalized_hot(c.site.kept_sr_ids, obey_age_limit=False)
|
|
else:
|
|
if c.site.sticky_fullname:
|
|
link_list = [c.site.sticky_fullname]
|
|
wrapped = wrap_links(link_list,
|
|
wrapper=self.builder_wrapper,
|
|
keep_fn=self.keep_fn(),
|
|
skip=True)
|
|
# add all other items and decrement count if sticky is visible
|
|
if wrapped.things:
|
|
link_list += [l for l in c.site.get_links('hot', 'all')
|
|
if l != c.site.sticky_fullname]
|
|
if not self.after:
|
|
self.count -= 1
|
|
self.num += 1
|
|
return link_list
|
|
|
|
# no sticky or sticky hidden
|
|
return c.site.get_links('hot', 'all')
|
|
|
|
def content(self):
|
|
# only send a spotlight listing for HTML rendering
|
|
if c.render_style == "html":
|
|
spotlight = None
|
|
show_sponsors = not (not c.user.pref_show_sponsors and c.user.gold)
|
|
show_organic = c.user.pref_organic
|
|
on_frontpage = isinstance(c.site, DefaultSR)
|
|
|
|
if on_frontpage:
|
|
self.extra_page_classes = \
|
|
self.extra_page_classes + ['front-page']
|
|
|
|
if self.requested_ad:
|
|
spotlight = self.make_requested_ad()
|
|
elif on_frontpage and show_organic:
|
|
spotlight = self.make_spotlight()
|
|
elif show_sponsors:
|
|
spotlight = self.make_single_ad()
|
|
|
|
if spotlight:
|
|
return PaneStack([spotlight, self.listing_obj],
|
|
css_class='spacer')
|
|
return self.listing_obj
|
|
|
|
def title(self):
|
|
return c.site.title
|
|
|
|
@require_oauth2_scope("read")
|
|
@listing_api_doc(uri='/hot', uses_site=True)
|
|
def GET_listing(self, **env):
|
|
self.requested_ad = request.GET.get('ad')
|
|
self.infotext = request.GET.get('deleted') and strings.user_deleted
|
|
return ListingController.GET_listing(self, **env)
|
|
|
|
class NewController(ListingController):
|
|
where = 'new'
|
|
title_text = _('newest submissions')
|
|
extra_page_classes = ListingController.extra_page_classes + ['new-page']
|
|
show_chooser = True
|
|
next_suggestions_cls = ListingSuggestions
|
|
|
|
def keep_fn(self):
|
|
def keep(item):
|
|
"""Avoid showing links that are too young, to give time
|
|
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 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:
|
|
# also let the author of the link see them
|
|
return True
|
|
else:
|
|
# otherwise, fall back to the regular logic (don't
|
|
# show hidden links, etc)
|
|
return wouldkeep
|
|
|
|
return keep
|
|
|
|
def query(self):
|
|
return c.site.get_links('new', 'all')
|
|
|
|
def POST_listing(self, **env):
|
|
# Redirect to GET mode in case of any legacy requests
|
|
return self.redirect(request.fullpath)
|
|
|
|
@require_oauth2_scope("read")
|
|
@listing_api_doc(uri='/new', uses_site=True)
|
|
def GET_listing(self, **env):
|
|
if request.params.get('sort') == 'rising':
|
|
return self.redirect(add_sr('/rising'))
|
|
|
|
return ListingController.GET_listing(self, **env)
|
|
|
|
class RisingController(NewController):
|
|
where = 'rising'
|
|
title_text = _('rising submissions')
|
|
extra_page_classes = ListingController.extra_page_classes + ['rising-page']
|
|
|
|
def query(self):
|
|
return get_rising(c.site)
|
|
|
|
class BrowseController(ListingController):
|
|
where = 'browse'
|
|
show_chooser = True
|
|
next_suggestions_cls = ListingSuggestions
|
|
|
|
def keep_fn(self):
|
|
"""For merged time-listings, don't show items that are too old
|
|
(this can happen when mr_top hasn't run in a while)"""
|
|
if self.time != 'all' and c.default_sr:
|
|
oldest = timeago('1 %s' % (str(self.time),))
|
|
def keep(item):
|
|
return item._date > oldest and item.keep_item(item)
|
|
return keep
|
|
else:
|
|
return ListingController.keep_fn(self)
|
|
|
|
@property
|
|
def menus(self):
|
|
return [ControversyTimeMenu(default = self.time)]
|
|
|
|
def query(self):
|
|
return c.site.get_links(self.sort, self.time)
|
|
|
|
@validate(t = VMenu('sort', ControversyTimeMenu))
|
|
def POST_listing(self, sort, t, **env):
|
|
# VMenu validator will save the value of time before we reach this
|
|
# point. Now just redirect to GET mode.
|
|
return self.redirect(
|
|
request.fullpath + query_string(dict(sort=sort, t=t)))
|
|
|
|
@require_oauth2_scope("read")
|
|
@validate(t = VMenu('sort', ControversyTimeMenu))
|
|
@listing_api_doc(uri='/{sort}', uri_variants=['/top', '/controversial'],
|
|
uses_site=True)
|
|
def GET_listing(self, sort, t, **env):
|
|
self.sort = sort
|
|
if sort == 'top':
|
|
self.title_text = _('top scoring links')
|
|
self.extra_page_classes = \
|
|
self.extra_page_classes + ['top-page']
|
|
elif sort == 'controversial':
|
|
self.title_text = _('most controversial links')
|
|
self.extra_page_classes = \
|
|
self.extra_page_classes + ['controversial-page']
|
|
else:
|
|
# 'sort' is forced to top/controversial by routing.py,
|
|
# but in case something has gone wrong...
|
|
abort(404)
|
|
self.time = t
|
|
return ListingController.GET_listing(self, **env)
|
|
|
|
|
|
class RandomrisingController(ListingController):
|
|
where = 'randomrising'
|
|
title_text = _('you\'re really bored now, eh?')
|
|
next_suggestions_cls = ListingSuggestions
|
|
|
|
def query(self):
|
|
links = get_rising(c.site)
|
|
|
|
if not links:
|
|
# just pull from the new page if the rising page isn't
|
|
# populated for some reason
|
|
links = c.site.get_links('new', 'all')
|
|
if isinstance(links, Query):
|
|
links._limit = 200
|
|
links = [x._fullname for x in links]
|
|
|
|
links = list(links)
|
|
random.shuffle(links)
|
|
|
|
return links
|
|
|
|
class ByIDController(ListingController):
|
|
title_text = _('API')
|
|
skip = False
|
|
|
|
def query(self):
|
|
return self.names
|
|
|
|
@require_oauth2_scope("read")
|
|
@validate(links=VByName("names", thing_cls=Link,
|
|
ignore_missing=True, multiple=True))
|
|
def GET_listing(self, links, **env):
|
|
if not links:
|
|
return self.abort404()
|
|
self.names = [l._fullname for l in links]
|
|
return ListingController.GET_listing(self, **env)
|
|
|
|
|
|
#class RecommendedController(ListingController):
|
|
# where = 'recommended'
|
|
# title_text = _('recommended for you')
|
|
#
|
|
# @property
|
|
# def menus(self):
|
|
# return [RecSortMenu(default = self.sort)]
|
|
#
|
|
# def query(self):
|
|
# return get_recommended(c.user._id, sort = self.sort)
|
|
#
|
|
# @validate(VUser(),
|
|
# sort = VMenu("controller", RecSortMenu))
|
|
# def GET_listing(self, sort, **env):
|
|
# self.sort = sort
|
|
# return ListingController.GET_listing(self, **env)
|
|
|
|
class UserController(ListingController):
|
|
render_cls = ProfilePage
|
|
show_nums = False
|
|
|
|
@property
|
|
def menus(self):
|
|
res = []
|
|
if (self.where in ('overview', 'submitted', 'comments')):
|
|
res.append(ProfileSortMenu(default = self.sort))
|
|
if self.sort not in ("hot", "new"):
|
|
res.append(TimeMenu(default = self.time))
|
|
if self.where == 'saved' and c.user.gold:
|
|
srnames = LinkSavesBySubreddit.get_saved_subreddits(self.vuser)
|
|
srnames += CommentSavesBySubreddit.get_saved_subreddits(self.vuser)
|
|
srs = Subreddit._by_name(srnames)
|
|
srnames = [name for name, sr in srs.iteritems()
|
|
if sr.can_view(c.user)]
|
|
srnames = sorted(list(set(srnames)))
|
|
if len(srnames) > 1:
|
|
sr_buttons = [NavButton(_('all'), None, opt='sr',
|
|
css_class='primary')]
|
|
for srname in srnames:
|
|
sr_buttons.append(NavButton(srname, srname, opt='sr'))
|
|
base_path = request.path
|
|
sr_menu = NavMenu(sr_buttons, base_path=base_path,
|
|
title=_('filter by subreddit'),
|
|
type='lightdrop')
|
|
res.append(sr_menu)
|
|
elif (self.where == 'gilded' and
|
|
(c.user == self.vuser or c.user_is_admin)):
|
|
path = '/user/%s/gilded/' % self.vuser.name
|
|
buttons = [NavButton(_("my posts"), dest='/'),
|
|
NavButton(_("posts gilded by me"), dest='/given')]
|
|
res.append(NavMenu(buttons, base_path=path, type='flatlist'))
|
|
|
|
return res
|
|
|
|
def title(self):
|
|
titles = {'overview': _("overview for %(user)s"),
|
|
'comments': _("comments by %(user)s"),
|
|
'submitted': _("submitted by %(user)s"),
|
|
'gilded': _("gilded comments by %(user)s"),
|
|
'liked': _("liked by %(user)s"),
|
|
'disliked': _("disliked by %(user)s"),
|
|
'saved': _("saved by %(user)s"),
|
|
'hidden': _("hidden by %(user)s"),
|
|
'promoted': _("promoted by %(user)s")}
|
|
if self.where == 'gilded' and self.show == 'given':
|
|
return _("comments gilded by %(user)s") % {'user': self.vuser.name}
|
|
|
|
title = titles.get(self.where, _('profile for %(user)s')) \
|
|
% 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):
|
|
if self.where == 'promoted':
|
|
return bool(getattr(item, "promoted", None))
|
|
|
|
wouldkeep = True
|
|
# TODO: Consider a flag to disable this (and see below plus builder.py)
|
|
if item._deleted and not c.user_is_admin:
|
|
return False
|
|
if self.time != 'all':
|
|
wouldkeep = (item._date > utils.timeago('1 %s' % str(self.time)))
|
|
if c.user == self.vuser:
|
|
if not item.likes and self.where == 'liked':
|
|
return False
|
|
if item.likes is not False and self.where == 'disliked':
|
|
return False
|
|
if self.where == 'saved' and not item.saved:
|
|
return False
|
|
if self.where == 'gilded':
|
|
wouldkeep = item.gildings > 0
|
|
|
|
return wouldkeep and (getattr(item, "promoted", None) is None and
|
|
(self.where == "deleted" or
|
|
not getattr(item, "deleted", False)))
|
|
return keep
|
|
|
|
def query(self):
|
|
q = None
|
|
if self.where == 'overview':
|
|
self.check_modified(self.vuser, 'overview')
|
|
q = queries.get_overview(self.vuser, self.sort, self.time)
|
|
|
|
elif self.where == 'comments':
|
|
sup.set_sup_header(self.vuser, 'commented')
|
|
self.check_modified(self.vuser, 'commented')
|
|
q = queries.get_comments(self.vuser, self.sort, self.time)
|
|
|
|
elif self.where == 'submitted':
|
|
sup.set_sup_header(self.vuser, 'submitted')
|
|
self.check_modified(self.vuser, 'submitted')
|
|
q = queries.get_submitted(self.vuser, self.sort, self.time)
|
|
|
|
elif self.where == 'gilded':
|
|
sup.set_sup_header(self.vuser, 'gilded')
|
|
self.check_modified(self.vuser, 'gilded')
|
|
if self.show == 'given':
|
|
q = queries.get_user_gildings(self.vuser)
|
|
else:
|
|
q = queries.get_gilded_user_comments(self.vuser)
|
|
|
|
elif self.where in ('liked', 'disliked'):
|
|
sup.set_sup_header(self.vuser, self.where)
|
|
self.check_modified(self.vuser, self.where)
|
|
if self.where == 'liked':
|
|
q = queries.get_liked(self.vuser)
|
|
else:
|
|
q = queries.get_disliked(self.vuser)
|
|
|
|
elif self.where == 'hidden':
|
|
q = queries.get_hidden(self.vuser)
|
|
|
|
elif self.where == 'saved':
|
|
srname = request.GET.get('sr')
|
|
if srname and c.user.gold:
|
|
try:
|
|
sr_id = Subreddit._by_name(srname)._id
|
|
except NotFound:
|
|
sr_id = None
|
|
else:
|
|
sr_id = None
|
|
q = queries.get_saved(self.vuser, sr_id)
|
|
|
|
elif c.user_is_sponsor and self.where == 'promoted':
|
|
q = queries.get_promoted_links(self.vuser._id)
|
|
|
|
if q is None:
|
|
return self.abort404()
|
|
|
|
return q
|
|
|
|
@require_oauth2_scope("history")
|
|
@validate(vuser = VExistingUname('username'),
|
|
sort = VMenu('sort', ProfileSortMenu, remember = False),
|
|
time = VMenu('t', TimeMenu, remember = False),
|
|
show=VOneOf('show', ('given',)))
|
|
@listing_api_doc(section=api_section.users, uri='/user/{username}/{where}',
|
|
uri_variants=['/user/{username}/' + where for where in [
|
|
'overview', 'submitted', 'comments',
|
|
'liked', 'disliked', 'hidden', 'saved',
|
|
'gilded']])
|
|
def GET_listing(self, where, vuser, sort, time, show, **env):
|
|
self.where = where
|
|
self.sort = sort
|
|
self.time = time
|
|
self.show = show
|
|
|
|
# the validator will ensure that vuser is a valid account
|
|
if not vuser:
|
|
return self.abort404()
|
|
|
|
if self.sort in ('hot', 'new'):
|
|
self.time = 'all'
|
|
|
|
|
|
# hide spammers profile pages
|
|
if (not c.user_is_loggedin or
|
|
(c.user._id != vuser._id and not c.user_is_admin)) \
|
|
and vuser._spam:
|
|
return self.abort404()
|
|
|
|
if where in ('liked', 'disliked') and not votes_visible(vuser):
|
|
return self.abort403()
|
|
|
|
if ((where in ('saved', 'hidden') or
|
|
(where == 'gilded' and show == 'given')) and
|
|
not (c.user_is_loggedin and c.user._id == vuser._id) and
|
|
not c.user_is_admin):
|
|
return self.abort403()
|
|
|
|
if where == 'saved':
|
|
self.show_chooser = True
|
|
|
|
check_cheating('user')
|
|
|
|
self.vuser = vuser
|
|
self.render_params = {'user' : vuser}
|
|
c.profilepage = True
|
|
|
|
if vuser.pref_hide_from_robots:
|
|
self.robots = 'noindex,nofollow'
|
|
|
|
return ListingController.GET_listing(self, **env)
|
|
|
|
@validate(vuser = VExistingUname('username'))
|
|
@api_doc(section=api_section.users, uri='/user/{username}/about',
|
|
extensions=['json'])
|
|
def GET_about(self, vuser):
|
|
"""Return information about the user, including karma and gold status."""
|
|
if not is_api() or not vuser:
|
|
return self.abort404()
|
|
return Reddit(content = Wrapped(vuser)).render()
|
|
|
|
def GET_saved_redirect(self):
|
|
if not c.user_is_loggedin:
|
|
abort(404)
|
|
|
|
dest = "/".join(("/user", c.user.name, "saved"))
|
|
extension = request.environ.get('extension')
|
|
if extension:
|
|
dest = ".".join((dest, extension))
|
|
query_string = request.environ.get('QUERY_STRING')
|
|
if query_string:
|
|
dest += "?" + query_string
|
|
return redirect_to(dest)
|
|
|
|
class MessageController(ListingController):
|
|
show_nums = False
|
|
render_cls = MessagePage
|
|
allow_stylesheets = False
|
|
# note: this intentionally replaces the listing-page class which doesn't
|
|
# conceptually fit for styling these pages.
|
|
extra_page_classes = ['messages-page']
|
|
|
|
@property
|
|
def show_sidebar(self):
|
|
if c.default_sr and not isinstance(c.site, (ModSR, MultiReddit)):
|
|
return False
|
|
|
|
return self.where in ("moderator", "multi")
|
|
|
|
@property
|
|
def menus(self):
|
|
if c.default_sr and self.where in ('inbox', 'messages', 'comments',
|
|
'selfreply', 'unread', 'mentions'):
|
|
buttons = [NavButton(_("all"), "inbox"),
|
|
NavButton(_("unread"), "unread"),
|
|
NavButton(plurals.messages, "messages"),
|
|
NavButton(_("comment replies"), 'comments'),
|
|
NavButton(_("post replies"), 'selfreply')]
|
|
|
|
if c.user.gold:
|
|
buttons += [NavButton(_("username mentions"),
|
|
"mentions",
|
|
css_class="gold")]
|
|
|
|
return [NavMenu(buttons, base_path = '/message/',
|
|
default = 'inbox', type = "flatlist")]
|
|
elif not c.default_sr or self.where in ('moderator', 'multi'):
|
|
buttons = (NavButton(_("all"), "inbox"),
|
|
NavButton(_("unread"), "unread"))
|
|
return [NavMenu(buttons, base_path = '/message/moderator/',
|
|
default = 'inbox', type = "flatlist")]
|
|
return []
|
|
|
|
|
|
def title(self):
|
|
return _('messages') + ': ' + _(self.where)
|
|
|
|
def keep_fn(self):
|
|
def keep(item):
|
|
wouldkeep = item.keep_item(item)
|
|
|
|
# TODO: Consider a flag to disable this (and see above plus builder.py)
|
|
if (item._deleted or item._spam) and not c.user_is_admin:
|
|
return False
|
|
if item.author_id in c.user.enemies:
|
|
return False
|
|
# don't show user their own unread stuff
|
|
if ((self.where == 'unread' or self.subwhere == 'unread')
|
|
and (item.author_id == c.user._id or not item.new)):
|
|
return False
|
|
|
|
if (item.message_style == "mention" and
|
|
c.user.name.lower() not in extract_user_mentions(item.body)):
|
|
return False
|
|
|
|
return wouldkeep
|
|
return keep
|
|
|
|
@staticmethod
|
|
def builder_wrapper(thing):
|
|
if isinstance(thing, Comment):
|
|
f = thing._fullname
|
|
w = Wrapped(thing)
|
|
w.render_class = Message
|
|
w.to_id = c.user._id
|
|
w.was_comment = True
|
|
w._fullname = f
|
|
else:
|
|
w = ListingController.builder_wrapper(thing)
|
|
|
|
return w
|
|
|
|
def builder(self):
|
|
if (self.where == 'messages' or
|
|
(self.where in ("moderator", "multi") and self.subwhere != "unread")):
|
|
root = c.user
|
|
message_cls = UserMessageBuilder
|
|
|
|
if self.where == "multi":
|
|
root = c.site
|
|
message_cls = MultiredditMessageBuilder
|
|
elif not c.default_sr:
|
|
root = c.site
|
|
message_cls = SrMessageBuilder
|
|
elif self.where == 'moderator' and self.subwhere != 'unread':
|
|
message_cls = ModeratorMessageBuilder
|
|
|
|
parent = None
|
|
skip = False
|
|
if self.message:
|
|
if self.message.first_message:
|
|
parent = Message._byID(self.message.first_message,
|
|
data=True)
|
|
else:
|
|
parent = self.message
|
|
elif c.user.pref_threaded_messages:
|
|
skip = (c.render_style == "html")
|
|
|
|
return message_cls(root,
|
|
wrap = self.builder_wrapper,
|
|
parent = parent,
|
|
skip = skip,
|
|
num = self.num,
|
|
after = self.after,
|
|
keep_fn = self.keep_fn(),
|
|
reverse = self.reverse)
|
|
return ListingController.builder(self)
|
|
|
|
def listing(self):
|
|
if (self.where == 'messages' and
|
|
(c.user.pref_threaded_messages or self.message)):
|
|
return Listing(self.builder_obj).listing()
|
|
pane = ListingController.listing(self)
|
|
|
|
# Indicate that the comment tree wasn't built for comments
|
|
for i in pane.things:
|
|
if i.was_comment:
|
|
i.child = None
|
|
|
|
return pane
|
|
|
|
def query(self):
|
|
if self.where == 'messages':
|
|
q = queries.get_inbox_messages(c.user)
|
|
elif self.where == 'comments':
|
|
q = queries.get_inbox_comments(c.user)
|
|
elif self.where == 'selfreply':
|
|
q = queries.get_inbox_selfreply(c.user)
|
|
elif self.where == 'mentions':
|
|
q = queries.get_inbox_comment_mentions(c.user)
|
|
elif self.where == 'inbox':
|
|
q = queries.get_inbox(c.user)
|
|
elif self.where == 'unread':
|
|
q = queries.get_unread_inbox(c.user)
|
|
elif self.where == 'sent':
|
|
q = queries.get_sent(c.user)
|
|
elif self.where == 'multi' and self.subwhere == 'unread':
|
|
q = queries.get_unread_subreddit_messages_multi(c.site.kept_sr_ids)
|
|
elif self.where == 'moderator' and self.subwhere == 'unread':
|
|
if c.default_sr:
|
|
srids = Subreddit.reverse_moderator_ids(c.user)
|
|
srs = [sr for sr in Subreddit._byID(srids, data=False,
|
|
return_dict=False)
|
|
if sr.is_moderator_with_perms(c.user, 'mail')]
|
|
q = queries.get_unread_subreddit_messages_multi(srs)
|
|
else:
|
|
q = queries.get_unread_subreddit_messages(c.site)
|
|
elif self.where in ('moderator', 'multi'):
|
|
if c.have_mod_messages and self.mark != 'false':
|
|
c.user.modmsgtime = False
|
|
c.user._commit()
|
|
# the query is handled by the builder on the moderator page
|
|
return
|
|
else:
|
|
return self.abort404()
|
|
if self.where != 'sent':
|
|
#reset the inbox
|
|
if c.have_messages and self.mark != 'false':
|
|
c.user.msgtime = False
|
|
c.user._commit()
|
|
|
|
return q
|
|
|
|
@require_oauth2_scope("privatemessages")
|
|
@validate(VUser(),
|
|
message = VMessageID('mid'),
|
|
mark = VOneOf('mark',('true','false')))
|
|
@listing_api_doc(section=api_section.messages,
|
|
uri='/message/{where}',
|
|
uri_variants=['/message/inbox', '/message/unread', '/message/sent'])
|
|
def GET_listing(self, where, mark, message, subwhere = None, **env):
|
|
if not (c.default_sr
|
|
or c.site.is_moderator_with_perms(c.user, 'mail')
|
|
or c.user_is_admin):
|
|
abort(403, "forbidden")
|
|
if isinstance(c.site, MultiReddit):
|
|
if not (c.user_is_admin or c.site.is_moderator(c.user)):
|
|
self.abort403()
|
|
self.where = "multi"
|
|
elif isinstance(c.site, ModSR) or not c.default_sr:
|
|
self.where = "moderator"
|
|
else:
|
|
self.where = where
|
|
self.subwhere = subwhere
|
|
if mark is not None:
|
|
self.mark = mark
|
|
elif is_api():
|
|
self.mark = 'false'
|
|
elif c.render_style and c.render_style == "xml":
|
|
self.mark = 'false'
|
|
else:
|
|
self.mark = 'true'
|
|
self.message = message
|
|
return ListingController.GET_listing(self, **env)
|
|
|
|
@validate(VUser(),
|
|
to = nop('to'),
|
|
subject = nop('subject'),
|
|
message = nop('message'),
|
|
success = nop('success'))
|
|
def GET_compose(self, to, subject, message, success):
|
|
captcha = Captcha() if c.user.needs_captcha() else None
|
|
content = MessageCompose(to = to, subject = subject,
|
|
captcha = captcha,
|
|
message = message,
|
|
success = success)
|
|
return MessagePage(content = content).render()
|
|
|
|
class RedditsController(ListingController):
|
|
render_cls = SubredditsPage
|
|
|
|
def title(self):
|
|
return _('subreddits')
|
|
|
|
def keep_fn(self):
|
|
base_keep_fn = ListingController.keep_fn(self)
|
|
def keep(item):
|
|
return base_keep_fn(item) and (c.over18 or not item.over_18)
|
|
return keep
|
|
|
|
def query(self):
|
|
if self.where == 'banned' and c.user_is_admin:
|
|
reddits = Subreddit._query(Subreddit.c._spam == True,
|
|
sort = desc('_date'),
|
|
write_cache = True,
|
|
read_cache = True,
|
|
cache_time = 5 * 60)
|
|
else:
|
|
reddits = None
|
|
if self.where == 'new':
|
|
reddits = Subreddit._query( write_cache = True,
|
|
read_cache = True,
|
|
cache_time = 5 * 60)
|
|
reddits._sort = desc('_date')
|
|
else:
|
|
reddits = Subreddit._query( write_cache = True,
|
|
read_cache = True,
|
|
cache_time = 60 * 60)
|
|
reddits._sort = desc('_downs')
|
|
# Consider resurrecting when it is not the World Cup
|
|
#if c.content_langs != 'all':
|
|
# reddits._filter(Subreddit.c.lang == c.content_langs)
|
|
|
|
if g.domain != 'reddit.com':
|
|
# don't try to render special subreddits (like promos)
|
|
reddits._filter(Subreddit.c.author_id != -1)
|
|
|
|
if self.where == 'popular':
|
|
self.render_params = {"show_interestbar": True}
|
|
|
|
return reddits
|
|
|
|
@listing_api_doc(section=api_section.subreddits,
|
|
uri='/subreddits/{where}',
|
|
uri_variants=['/subreddits/popular', '/subreddits/new', '/subreddits/banned'])
|
|
def GET_listing(self, where, **env):
|
|
self.where = where
|
|
return ListingController.GET_listing(self, **env)
|
|
|
|
class MyredditsController(ListingController, OAuth2ResourceController):
|
|
render_cls = MySubredditsPage
|
|
|
|
def pre(self):
|
|
self.check_for_bearer_token()
|
|
ListingController.pre(self)
|
|
|
|
@property
|
|
def menus(self):
|
|
buttons = (NavButton(plurals.subscriber, 'subscriber'),
|
|
NavButton(getattr(plurals, "approved submitter"), 'contributor'),
|
|
NavButton(plurals.moderator, 'moderator'))
|
|
|
|
return [NavMenu(buttons, base_path = '/subreddits/mine/',
|
|
default = 'subscriber', type = "flatlist")]
|
|
|
|
def title(self):
|
|
return _('subreddits: ') + self.where
|
|
|
|
def builder_wrapper(self, thing):
|
|
w = ListingController.builder_wrapper(thing)
|
|
if self.where == 'moderator':
|
|
is_moderator = thing.is_moderator(c.user)
|
|
if is_moderator:
|
|
w.mod_permissions = is_moderator.get_permissions()
|
|
return w
|
|
|
|
def query(self):
|
|
reddits = SRMember._query(SRMember.c._name == self.where,
|
|
SRMember.c._thing2_id == c.user._id,
|
|
#hack to prevent the query from
|
|
#adding it's own date
|
|
sort = (desc('_t1_ups'), desc('_t1_date')),
|
|
eager_load = True,
|
|
thing_data = True)
|
|
reddits.prewrap_fn = lambda x: x._thing1
|
|
return reddits
|
|
|
|
def content(self):
|
|
user = c.user if c.user_is_loggedin else None
|
|
num_subscriptions = len(Subreddit.reverse_subscriber_ids(user))
|
|
if self.where == 'subscriber' and num_subscriptions == 0:
|
|
message = strings.sr_messages['empty']
|
|
else:
|
|
message = strings.sr_messages.get(self.where)
|
|
|
|
stack = PaneStack()
|
|
|
|
if message:
|
|
stack.append(InfoBar(message=message))
|
|
|
|
stack.append(self.listing_obj)
|
|
|
|
return stack
|
|
|
|
def build_listing(self, after=None, **kwargs):
|
|
if after and isinstance(after, Subreddit):
|
|
after = SRMember._fast_query(after, c.user, self.where,
|
|
data=False).values()[0]
|
|
if after and not isinstance(after, SRMember):
|
|
abort(400, 'gimme a srmember')
|
|
|
|
return ListingController.build_listing(self, after=after, **kwargs)
|
|
|
|
@require_oauth2_scope("mysubreddits")
|
|
@validate(VUser())
|
|
@listing_api_doc(section=api_section.subreddits,
|
|
uri='/subreddits/mine/{where}',
|
|
uri_variants=['/subreddits/mine/subscriber', '/subreddits/mine/contributor', '/subreddits/mine/moderator'])
|
|
def GET_listing(self, where='subscriber', **env):
|
|
self.where = where
|
|
return ListingController.GET_listing(self, **env)
|
|
|
|
class CommentsController(ListingController):
|
|
title_text = _('comments')
|
|
|
|
def keep_fn(self):
|
|
def keep(item):
|
|
can_see_spam = (c.user_is_loggedin and
|
|
(item.author_id == c.user._id or
|
|
c.user_is_admin or
|
|
item.subreddit.is_moderator(c.user)))
|
|
can_see_deleted = c.user_is_loggedin and c.user_is_admin
|
|
|
|
return ((not item._spam or can_see_spam) and
|
|
(not item._deleted or can_see_deleted))
|
|
|
|
return keep
|
|
|
|
def query(self):
|
|
return c.site.get_all_comments()
|
|
|
|
@require_oauth2_scope("read")
|
|
def GET_listing(self, **env):
|
|
c.profilepage = True
|
|
return ListingController.GET_listing(self, **env)
|
|
|
|
|
|
class GildedController(ListingController):
|
|
title_text = _("gilded comments")
|
|
|
|
def keep_fn(self):
|
|
def keep(item):
|
|
return item.gildings > 0 and not item._deleted and not item._spam
|
|
return keep
|
|
|
|
def query(self):
|
|
try:
|
|
return c.site.get_gilded_comments()
|
|
except NotImplementedError:
|
|
abort(404)
|
|
|
|
@require_oauth2_scope("read")
|
|
def GET_listing(self, **env):
|
|
c.profilepage = True
|
|
return ListingController.GET_listing(self, **env)
|