From 9ecb228d538d359a34a4d43fe6e507a4ea5fb67e Mon Sep 17 00:00:00 2001 From: Andre D Date: Thu, 23 Jan 2014 20:11:16 -0500 Subject: [PATCH] /about: Paginate banned and contibutors. --- r2/r2/config/routing.py | 5 + r2/r2/config/templates.py | 6 +- r2/r2/controllers/__init__.py | 1 + r2/r2/controllers/api.py | 44 ++- r2/r2/controllers/front.py | 42 +-- r2/r2/controllers/listingcontroller.py | 206 ++++++++++- r2/r2/lib/jsontemplates.py | 11 +- r2/r2/lib/menus.py | 1 + r2/r2/lib/pages/pages.py | 346 ++++++------------ r2/r2/lib/pages/trafficpages.py | 4 + r2/r2/models/builder.py | 9 + r2/r2/models/listing.py | 151 ++++++++ r2/r2/public/static/css/reddit.less | 10 + .../{modlist.html => modlisting.html} | 18 +- r2/r2/templates/multiinfobar.html | 2 +- r2/r2/templates/subredditinfobar.html | 5 +- r2/r2/templates/userlisting.html | 138 +++++++ r2/r2/templates/usertableitem.html | 2 +- 18 files changed, 691 insertions(+), 310 deletions(-) rename r2/r2/templates/{modlist.html => modlisting.html} (78%) create mode 100644 r2/r2/templates/userlisting.html diff --git a/r2/r2/config/routing.py b/r2/r2/config/routing.py index eece8828e..587a7626d 100644 --- a/r2/r2/config/routing.py +++ b/r2/r2/config/routing.py @@ -160,6 +160,9 @@ def make_map(): connect('/about/:location', controller='front', action='spamlisting', requirements=dict(location='reports|spam|modqueue|unmoderated')) + connect('/about/:where', controller='userlistlisting', + requirements=dict(where='contributors|banned|wikibanned|wikicontributors|moderators'), + action='listing') connect('/about/:location', controller='front', action='editreddit', location='about') connect('/comments', controller='comments', action='listing') @@ -175,6 +178,8 @@ def make_map(): mc('/t/:timereddit/*rest', controller='redirect', action='timereddit_redirect') + mc('/prefs/:where', controller='userlistlisting', + action='listing', requirements=dict(where='blocked|friends')) mc('/prefs/:location', controller='forms', action='prefs', location='options') diff --git a/r2/r2/config/templates.py b/r2/r2/config/templates.py index 8ba768a4a..4ee18630a 100644 --- a/r2/r2/config/templates.py +++ b/r2/r2/config/templates.py @@ -45,11 +45,7 @@ api('morechildren', MoreCommentJsonTemplate) api('reddit', RedditJsonTemplate) api('panestack', PanestackJsonTemplate) api('listing', ListingJsonTemplate) -api('modlist', UserListJsonTemplate) -api('userlist', UserListJsonTemplate) -api('contributorlist', UserListJsonTemplate) -api('bannedlist', UserListJsonTemplate) -api('friendlist', UserListJsonTemplate) +api('userlisting', UserListingJsonTemplate) api('usertableitem', UserTableItemJsonTemplate) api('account', AccountJsonTemplate) diff --git a/r2/r2/controllers/__init__.py b/r2/r2/controllers/__init__.py index 1f32d7c18..8ecbbdf15 100644 --- a/r2/r2/controllers/__init__.py +++ b/r2/r2/controllers/__init__.py @@ -44,6 +44,7 @@ def load_controllers(): from listingcontroller import NewController from listingcontroller import RisingController from listingcontroller import BrowseController + from listingcontroller import UserListListingController from listingcontroller import MessageController from listingcontroller import RedditsController from listingcontroller import ByIDController diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index a6c6fd4c2..da1a260c6 100755 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -44,13 +44,21 @@ from r2.lib import hooks from r2.lib.utils import get_title, sanitize_url, timeuntil, set_last_modified from r2.lib.utils import query_string, timefromnow, randstr from r2.lib.utils import timeago, tup -from r2.lib.pages import (EnemyList, FriendList, ContributorList, ModList, - BannedList, WikiBannedList, WikiMayContributeList, - BoringPage, FormPage, CssError, UploadedImage, +from r2.lib.pages import (BoringPage, FormPage, CssError, UploadedImage, ClickGadget, UrlParser, WrappedUser) from r2.lib.pages import FlairList, FlairCsv, FlairTemplateEditor, \ FlairSelector from r2.lib.pages import PrefApps +from r2.lib.pages import ( + BannedTableItem, + ContributorTableItem, + FriendTableItem, + InvitedModTableItem, + ModTableItem, + WikiBannedTableItem, + WikiMayContributeTableItem, +) + from r2.lib.pages.things import ( default_thing_wrapper, hot_links_by_url_listing, @@ -866,22 +874,28 @@ class ApiController(RedditController): if type in ('banned', 'wikibanned'): container.add_rel_note(type, friend, note) - cls = dict(friend=FriendList, - moderator=ModList, - moderator_invite=ModList, - contributor=ContributorList, - wikicontributor=WikiMayContributeList, - banned=BannedList, wikibanned=WikiBannedList).get(type) - userlist = cls() + row_cls = dict(friend=FriendTableItem, + moderator=ModTableItem, + moderator_invite=InvitedModTableItem, + contributor=ContributorTableItem, + wikicontributor=WikiMayContributeTableItem, + banned=BannedTableItem, + wikibanned=WikiBannedTableItem).get(type) + form.set_inputs(name = "") if note: form.set_inputs(note = "") form.removeClass("edited") - form.set_html(".status:first", userlist.executed_message(type)) - if new and cls: - user_row = userlist.user_row(type, friend) - jquery("." + type + "-table").show( - ).find("table").insert_table_rows(user_row) + + if new and row_cls: + new._thing2 = friend + user_row = row_cls(new) + form.set_html(".status:first", user_row.executed_message) + rev_types = ["moderator", "moderator_invite", "friend"] + index = 0 if user_row.type not in rev_types else -1 + table = jquery("." + type + "-table").show().find("table") + table.insert_table_rows(user_row, index=index) + table.find(".notfound").hide() if new: notify_user_added(type, c.user, friend, container) diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py index 372c36759..4064808e3 100755 --- a/r2/r2/controllers/front.py +++ b/r2/r2/controllers/front.py @@ -651,42 +651,14 @@ class FrontController(RedditController): return pane def _edit_normal_reddit(self, location, created): - # moderator is either reddit's moderator or an admin - moderator_rel = c.user_is_loggedin and c.site.get_moderator(c.user) - is_moderator = c.user_is_admin or moderator_rel - is_unlimited_moderator = c.user_is_admin or ( - moderator_rel and moderator_rel.is_superuser()) - is_moderator_with_perms = lambda *perms: ( - c.user_is_admin - or moderator_rel and all(moderator_rel.has_permission(perm) - for perm in perms)) - - if is_moderator_with_perms('config') and location == 'edit': + if (location == 'edit' and + (c.user_is_admin or c.site.is_moderator_with_perms(c.user, 'config'))): pane = PaneStack() if created == 'true': pane.append(InfoBar(message=strings.sr_created)) c.allow_styles = True c.site = Subreddit._byID(c.site._id, data=True, stale=False) pane.append(CreateSubreddit(site=c.site)) - elif location == 'moderators': - pane = ModList(editable=is_unlimited_moderator) - elif is_moderator_with_perms('access') and location == 'banned': - pane = BannedList(editable=is_moderator_with_perms('access')) - elif is_moderator_with_perms('wiki') and location == 'wikibanned': - pane = WikiBannedList(editable=is_moderator_with_perms('wiki')) - elif (is_moderator_with_perms('wiki') - and location == 'wikicontributors'): - pane = WikiMayContributeList( - editable=is_moderator_with_perms('wiki')) - elif (location == 'contributors' and - # On public reddits, only moderators can see the whitelist. - # On private reddits, all contributors can see each other. - (c.site.type != 'public' or - (c.user_is_loggedin and - (c.site.is_moderator_with_perms(c.user, 'access') - or c.user_is_admin)))): - pane = ContributorList( - editable=is_moderator_with_perms('access')) elif (location == 'stylesheet' and c.site.can_change_stylesheet(c.user) and not g.css_killswitch): @@ -706,17 +678,14 @@ class FrontController(RedditController): c.site.stylesheet_contents) pane = SubredditStylesheetSource(stylesheet_contents=stylesheet) elif location == 'traffic' and (c.site.public_traffic or - (is_moderator or c.user.employee)): + (c.site.is_moderator(c.user) or c.user.employee)): pane = trafficpages.SubredditTraffic() elif (location == "about") and is_api(): return self.redirect(add_sr('about.json'), code=301) else: return self.abort404() - is_wiki_action = location in ['wikibanned', 'wikicontributors'] - return EditReddit(content=pane, - show_wiki_actions=is_wiki_action, location=location, extension_handling=False).render() @@ -1324,11 +1293,6 @@ class FormsController(RedditController): infotext = None if not location or location == 'options': content = PrefOptions(done=request.GET.get('done')) - elif location == 'friends': - content = PaneStack() - infotext = strings.friends % Friends.path - content.append(FriendList()) - content.append(EnemyList()) elif location == 'update': if verified: infotext = strings.email_verified diff --git a/r2/r2/controllers/listingcontroller.py b/r2/r2/controllers/listingcontroller.py index e5b4ad666..af7385c56 100755 --- a/r2/r2/controllers/listingcontroller.py +++ b/r2/r2/controllers/listingcontroller.py @@ -29,7 +29,7 @@ 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.menus import ControversyTimeMenu, menu from r2.lib.rising import get_rising from r2.lib.wrapped import Wrapped from r2.lib.normalized_hot import normalized_hot @@ -115,6 +115,7 @@ class ListingController(RedditController): show_chooser=self.show_chooser, nav_menus=self.menus, title=self.title(), + infotext=self.infotext, robots=getattr(self, "robots", None), **self.render_params).render() @@ -1129,6 +1130,209 @@ class CommentsController(ListingController): c.profilepage = True return ListingController.GET_listing(self, **env) +class UserListListingController(ListingController): + builder_cls = UserListBuilder + allow_stylesheets = False + skip = False + + @property + def infotext(self): + if self.where == 'friends': + return strings.friends % Friends.path + elif self.where == 'blocked': + return _("To block a user click 'block user' below a message" + " from a user you wish to block from messaging you.") + + @property + def render_params(self): + params = {} + is_wiki_action = self.where in ["wikibanned", "wikicontributors"] + params["show_wiki_actions"] = is_wiki_action + return params + + @property + def render_cls(self): + if self.where in ["friends", "blocked"]: + return PrefsPage + return Reddit + + def moderator_wrap(self, rel, invited=False): + rel._permission_class = ModeratorPermissionSet + cls = ModTableItem if not invited else InvitedModTableItem + return cls(rel, editable=self.editable) + + @property + def builder_wrapper(self): + if self.where == 'banned': + cls = BannedTableItem + elif self.where == 'moderators': + return self.moderator_wrap + elif self.where == 'wikibanned': + cls = WikiBannedTableItem + elif self.where == 'contributors': + cls = ContributorTableItem + elif self.where == 'wikicontributors': + cls = WikiMayContributeTableItem + elif self.where == 'friends': + cls = FriendTableItem + elif self.where == 'blocked': + cls = EnemyTableItem + return lambda rel : cls(rel, editable=self.editable) + + def title(self): + return menu[self.where] + + def rel(self): + if self.where in ['friends', 'blocked']: + return Friend + return SRMember + + def name(self): + return self._names.get(self.where) + + _names = { + 'friends': 'friend', + 'blocked': 'enemy', + 'moderators': 'moderator', + 'contributors': 'contributor', + 'banned': 'banned', + 'wikibanned': 'wikibanned', + 'wikicontributors': 'wikicontributor', + } + + def query(self): + rel = self.rel() + if self.where in ["friends", "blocked"]: + thing1_id = c.user._id + else: + thing1_id = c.site._id + reversed_types = ["friends", "moderators", "blocked"] + sort = desc if self.where not in reversed_types else asc + q = rel._query(rel.c._thing1_id == thing1_id, + rel.c._name == self.name(), + sort=sort('_date'), + data=True) + if self.jump_to_val: + thing2_id = self.user._id if self.user else None + q._filter(rel.c._thing2_id == thing2_id) + return q + + def listing(self): + listing = self.listing_cls(self.builder_obj, + addable=self.editable, + show_jump_to=self.show_jump_to, + jump_to_value=self.jump_to_val, + show_not_found=self.show_not_found, + nextprev=self.paginated, + has_add_form=self.editable) + return listing.listing() + + def invited_mod_listing(self): + query = SRMember._query(SRMember.c._name == 'moderator_invite', + SRMember.c._thing1_id == c.site._id, + sort=asc('_date'), data=True) + wrapper = lambda rel: self.moderator_wrap(rel, invited=True) + b = self.builder_cls(query, + keep_fn=self.keep_fn(), + wrap=wrapper, + skip=False, + num=0) + return InvitedModListing(b, nextprev=False).listing() + + def content(self): + is_api = c.render_style in extensions.API_TYPES + if self.where == 'moderators' and self.editable and not is_api: + # Do not stack the invited mod list in api mode + # to allow for api compatibility with older api users. + content = PaneStack() + content.append(self.listing_obj) + content.append(self.invited_mod_listing()) + elif self.where == 'friends' and is_api: + content = PaneStack() + content.append(self.listing_obj) + empty_builder = IDBuilder([]) + # Append an empty UserList on the api for backwards + # compatibility with the old blocked list. + content.append(UserListing(empty_builder, nextprev=False).listing()) + else: + content = self.listing_obj + return content + + @require_oauth2_scope("read") + @validate(user=VAccountByName('user')) + @base_listing + def GET_listing(self, where, user=None, **kw): + allow_on_fake_sr = ["blocked", "friends"] + if isinstance(c.site, FakeSubreddit) and not where in allow_on_fake_sr: + return self.abort404() + + self.where = where + + has_mod_access = ((c.user_is_loggedin and + c.site.is_moderator_with_perms(c.user, 'access')) + or c.user_is_admin) + + if not c.user_is_loggedin and where not in ['contributors', 'moderators']: + abort(403) + + self.listing_cls = None + self.editable = True + self.paginated = True + self.jump_to_val = request.GET.get('user') + self.show_not_found = bool(self.jump_to_val) + + if where == 'contributors': + # On public reddits, only moderators may see the whitelist. + if c.site.type == 'public' and not has_mod_access: + abort(403) + # Used for subreddits like /r/lounge + if c.site.hide_subscribers: + abort(403) + self.listing_cls = ContributorListing + self.editable = has_mod_access + + elif where == 'banned': + if not has_mod_access: + abort(403) + self.listing_cls = BannedListing + + elif where == 'wikibanned': + if not c.site.is_moderator_with_perms(c.user, 'wiki'): + abort(403) + self.listing_cls = WikiBannedListing + + elif where == 'wikicontributors': + if not c.site.is_moderator_with_perms(c.user, 'wiki'): + abort(403) + self.listing_cls = WikiMayContributeListing + + elif where == 'moderators': + self.editable = ((c.user_is_loggedin and + c.site.is_unlimited_moderator(c.user)) or + c.user_is_admin) + self.listing_cls = ModListing + self.paginated = False + + elif where == 'friends': + self.listing_cls = FriendListing + self.paginated = False + + elif where == 'blocked': + self.listing_cls = EnemyListing + self.paginated = False + self.show_not_found = True + + if not self.listing_cls: + abort(404) + + self.user = user + self.show_jump_to = self.paginated + + if not self.paginated: + kw['num'] = 0 + + check_cheating('site') + return self.build_listing(**kw) class GildedController(ListingController): title_text = _("gilded comments") diff --git a/r2/r2/lib/jsontemplates.py b/r2/r2/lib/jsontemplates.py index 93f245ff1..4b47f4668 100755 --- a/r2/r2/lib/jsontemplates.py +++ b/r2/r2/lib/jsontemplates.py @@ -688,6 +688,15 @@ class ListingJsonTemplate(ThingJsonTemplate): def kind(self, wrapped): return "Listing" +class UserListingJsonTemplate(ListingJsonTemplate): + def raw_data(self, thing): + if not thing.nextprev: + return {"children": self.rendered_data(thing)} + return ListingJsonTemplate.raw_data(self, thing) + + def kind(self, wrapped): + return "Listing" if wrapped.nextprev else "UserList" + class UserListJsonTemplate(ThingJsonTemplate): _data_attrs_ = dict( children="users", @@ -722,7 +731,7 @@ class UserTableItemJsonTemplate(ThingJsonTemplate): (c.user.gold and thing.type == "friend")): d["note"] = getattr(thing.rel, 'note', '') if thing.type == "moderator": - permissions = thing.rel.permissions.iteritems() + permissions = thing.permissions.items() d["mod_permissions"] = [perm for perm, has in permissions if has] return d diff --git a/r2/r2/lib/menus.py b/r2/r2/lib/menus.py index 208e8a059..5ca3a8e52 100644 --- a/r2/r2/lib/menus.py +++ b/r2/r2/lib/menus.py @@ -99,6 +99,7 @@ menu = MenuHandler(hot = _('hot'), apps = _("apps"), feeds = _("RSS feeds"), friends = _("friends"), + blocked = _("blocked"), update = _("password/email"), delete = _("delete"), otp = _("two-factor authentication"), diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 811f2176b..1870097a7 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -866,10 +866,10 @@ class PrefsPage(Reddit): extension_handling = False - def __init__(self, show_sidebar = False, *a, **kw): + def __init__(self, show_sidebar = False, title=None, *a, **kw): + title = title or "%s (%s)" % (_("preferences"), c.site.name.strip(' ')) Reddit.__init__(self, show_sidebar = show_sidebar, - title = "%s (%s)" %(_("preferences"), - c.site.name.strip(' ')), + title=title, *a, **kw) def build_toolbars(self): @@ -880,6 +880,7 @@ class PrefsPage(Reddit): buttons.append(NamedButton('feeds')) buttons.extend([NamedButton('friends'), + NamedButton('blocked'), NamedButton('update')]) if c.user_is_loggedin and c.user.name in g.admins: @@ -2908,27 +2909,117 @@ class WrappedUser(CachedTemplate): fullname = user._fullname, user_deleted = user._deleted) -# Classes for dealing with friend/moderator/contributor/banned lists - - class UserTableItem(Templated): - """A single row in a UserList of type 'type' and of name - 'container_name' for a given user. The provided list of 'cells' - will determine what order the different columns are rendered in.""" - def __init__(self, user, type, cellnames, container_name, editable, - remove_action, rel=None): + type = '' + remove_action = 'unfriend' + cells = ('user', 'sendmessage', 'remove') + + @property + def executed_message(self): + return _("added") + + def __init__(self, user, editable=True, **kw): self.user = user - self.type = type - self.cells = cellnames - self.rel = rel - self.container_name = container_name - self.editable = editable - self.remove_action = remove_action - Templated.__init__(self) + self.editable = editable + Templated.__init__(self, **kw) def __repr__(self): return '' % self.user.name +class TrafficTableItem(UserTableItem): + type = "traffic_viewer" + remove_action = "rm_traffic_viewer" + + @property + def container_name(self): + return self.link._fullname + +class RelTableItem(UserTableItem): + def __init__(self, rel, **kw): + self._id = rel._id + self.rel = rel + UserTableItem.__init__(self, rel._thing2, **kw) + + @property + def container_name(self): + return c.site._fullname + +class FriendTableItem(RelTableItem): + type = 'friend' + + @property + def cells(self): + if c.user.gold: + return ('user', 'sendmessage', 'note', 'age', 'remove') + return ('user', 'sendmessage', 'remove') + + @property + def container_name(self): + return c.user._fullname + +class EnemyTableItem(RelTableItem): + type = 'enemy' + cells = ('user', 'remove') + + @property + def container_name(self): + return c.user._fullname + +class BannedTableItem(RelTableItem): + type = 'banned' + cells = ('user', 'sendmessage', 'remove', 'note') + + @property + def executed_message(self): + return _("banned") + +class WikiBannedTableItem(BannedTableItem): + type = 'wikibanned' + +class ContributorTableItem(RelTableItem): + type = 'contributor' + +class WikiMayContributeTableItem(RelTableItem): + type = 'wikicontributor' + +class InvitedModTableItem(RelTableItem): + type = 'moderator_invite' + cells = ('user', 'permissions', 'permissionsctl') + + @property + def executed_message(self): + return _("invited") + + def is_editable(self, user): + if not c.user_is_loggedin: + return False + elif c.user_is_admin: + return True + return c.site.is_unlimited_moderator(c.user) + + def __init__(self, rel, editable=True, **kw): + if editable: + self.cells += ('remove',) + editable = self.is_editable(rel._thing2) + self.permissions = ModeratorPermissions(rel._thing2, self.type, + rel.get_permissions(), + editable=editable) + RelTableItem.__init__(self, rel, editable=editable, **kw) + +class ModTableItem(InvitedModTableItem): + type = 'moderator' + + @property + def executed_message(self): + return _("added") + + def is_editable(self, user): + if not c.user_is_loggedin: + return False + elif c.user_is_admin: + return True + return c.user != user and c.site.can_demod(c.user, user) + class FlairPane(Templated): def __init__(self, num, after, reverse, name, user): # Make sure c.site isn't stale before rendering. @@ -3205,11 +3296,7 @@ class UserList(Templated): Templated.__init__(self) def user_row(self, row_type, user, editable=True): - """Convenience method for constructing a UserTableItem - instance of the user with type, container_name, etc. of this - UserList instance""" - return UserTableItem(user, row_type, self.cells, self.container_name, - editable, self.remove_action) + raise NotImplementedError def _user_rows(self, row_type, uids, editable_fn=None): """Generates a UserTableItem wrapped list of the Account @@ -3242,216 +3329,6 @@ class UserList(Templated): def executed_message(self, row_type): return _("added") - -class FriendList(UserList): - """Friend list on /pref/friends""" - type = 'friend' - - def __init__(self, editable = True): - if c.user.gold: - self.friend_rels = c.user.friend_rels() - self.cells = ('user', 'sendmessage', 'note', 'age', 'remove') - self._class = "gold-accent rounded" - self.table_headers = (_('user'), '', _('note'), _('friendship'), '') - - UserList.__init__(self) - - @property - def form_title(self): - return _('add a friend') - - @property - def table_title(self): - return _('your friends') - - def user_ids(self): - return c.user.friends - - def user_row(self, row_type, user, editable=True): - if not getattr(self, "friend_rels", None): - return UserList.user_row(self, row_type, user, editable) - else: - rel = self.friend_rels[user._id] - return UserTableItem(user, row_type, self.cells, self.container_name, - editable, self.remove_action, rel) - - @property - def container_name(self): - return c.user._fullname - - -class EnemyList(UserList): - """Blacklist on /pref/friends""" - type = 'enemy' - cells = ('user', 'remove') - - def __init__(self, editable=True, addable=False): - UserList.__init__(self, editable, addable) - - @property - def table_title(self): - return _('blocked users') - - def user_ids(self): - return c.user.enemies - - @property - def container_name(self): - return c.user._fullname - - -class ContributorList(UserList): - """Contributor list on a restricted/private reddit.""" - type = 'contributor' - - @property - def form_title(self): - return _("add approved submitter") - - @property - def table_title(self): - return _("approved submitters for %(reddit)s") % dict(reddit = c.site.name) - - def user_ids(self): - if c.site.hide_subscribers: - return [] # /r/lounge has too many subscribers to load without timing out, - # and besides, some people might not want this list to be so - # easily accessible. - else: - return c.site.contributors - -class ModList(UserList): - """Moderator list for a reddit.""" - type = 'moderator' - invite_type = 'moderator_invite' - invite_action = 'accept_moderator_invite' - form_title = _('add moderator') - invite_form_title = _('invite moderator') - remove_self_title = _('you are a moderator of this subreddit. %(action)s') - - def __init__(self, editable=True): - super(ModList, self).__init__(editable=editable) - self.perms_by_type = { - self.type: c.site.moderators_with_perms(), - self.invite_type: c.site.moderator_invites_with_perms(), - } - self.cells = ('user', 'permissions', 'permissionsctl') - if editable: - self.cells += ('remove',) - - @property - def table_title(self): - return _("moderators of /r/%(reddit)s") % {"reddit": c.site.name} - - def executed_message(self, row_type): - if row_type == "moderator_invite": - return _("invited") - else: - return _("added") - - @property - def can_force_add(self): - return c.user_is_admin - - @property - def can_remove_self(self): - return c.user_is_loggedin and c.site.is_moderator(c.user) - - @property - def has_invite(self): - return c.user_is_loggedin and c.site.is_moderator_invite(c.user) - - def moderator_editable(self, user, row_type): - if not c.user_is_loggedin: - return False - elif c.user_is_admin: - return True - elif row_type == self.type: - return c.user != user and c.site.can_demod(c.user, user) - elif row_type == self.invite_type: - return c.site.is_unlimited_moderator(c.user) - else: - return False - - def user_row(self, row_type, user, editable=True): - perms = ModeratorPermissions( - user, row_type, self.perms_by_type[row_type].get(user._id), - editable=editable) - return UserTableItem(user, row_type, self.cells, self.container_name, - editable, self.remove_action, rel=perms) - - @property - def user_rows(self): - return self._user_rows( - self.type, self.user_ids(), - lambda u: self.moderator_editable(u, self.type)) - - @property - def invited_user_rows(self): - return self._user_rows( - self.invite_type, self.invited_user_ids(), - lambda u: self.moderator_editable(u, self.invite_type)) - - def _sort_user_ids(self, row_type): - for user_id, perms in self.perms_by_type[row_type].iteritems(): - if perms is None: - yield user_id - for user_id, perms in self.perms_by_type[row_type].iteritems(): - if perms is not None: - yield user_id - - def user_ids(self): - return list(self._sort_user_ids(self.type)) - - def invited_user_ids(self): - return list(self._sort_user_ids(self.invite_type)) - -class BannedList(UserList): - """List of users banned from a given reddit""" - type = 'banned' - - def __init__(self, *k, **kw): - UserList.__init__(self, *k, **kw) - rels = getattr(c.site, 'each_%s' % self.type) - self.rels = OrderedDict((rel._thing2_id, rel) for rel in rels(data=True)) - self.cells += ('note',) - - def user_row(self, row_type, user, editable=True): - rel = self.rels.get(user._id, None) - return UserTableItem(user, row_type, self.cells, self.container_name, - editable, self.remove_action, rel) - - @property - def form_title(self): - return _('ban users') - - @property - def table_title(self): - return _('banned users') - - def user_ids(self): - return self.rels.keys() - -class WikiBannedList(BannedList): - """List of users banned from editing a given wiki""" - type = 'wikibanned' - -class WikiMayContributeList(UserList): - """List of users allowed to contribute to a given wiki""" - type = 'wikicontributor' - - @property - def form_title(self): - return _('add a wiki contributor') - - @property - def table_title(self): - return _('wiki page contributors') - - def user_ids(self): - return c.site.wikicontributor - - class DetailsPage(LinkInfoPage): extension_handling= False @@ -4337,6 +4214,9 @@ class ModeratorPermissions(Templated): Templated.__init__(self, permissions_type=permissions_type, editable=editable, embedded=embedded) + def items(self): + return self.permissions.iteritems() + class ListingChooser(Templated): def __init__(self): Templated.__init__(self) diff --git a/r2/r2/lib/pages/trafficpages.py b/r2/r2/lib/pages/trafficpages.py index d214f0f0f..455cc0461 100644 --- a/r2/r2/lib/pages/trafficpages.py +++ b/r2/r2/lib/pages/trafficpages.py @@ -37,6 +37,7 @@ from r2.lib.db.sorts import epoch_seconds from r2.lib.menus import menu from r2.lib.menus import NavButton, NamedButton, PageNameNav, NavMenu from r2.lib.pages.pages import Reddit, TimeSeriesChart, UserList, TabbedPane +from r2.lib.pages import TrafficTableItem from r2.lib.promote import cost_per_mille, cost_per_click from r2.lib.template_helpers import format_number from r2.lib.utils import Storage, to_date, timedelta_by_name @@ -764,6 +765,9 @@ class TrafficViewerList(UserList): def container_name(self): return self.link._fullname + def user_row(self, row_type, user, editable=True): + return TrafficTableItem(user) + class SubredditTrafficReport(Templated): def __init__(self): diff --git a/r2/r2/models/builder.py b/r2/r2/models/builder.py index b98856651..e615b57bb 100755 --- a/r2/r2/models/builder.py +++ b/r2/r2/models/builder.py @@ -737,3 +737,12 @@ class UserMessageBuilder(MessageBuilder): return conversation(self.user, self.parent) return user_messages(self.user) +class UserListBuilder(QueryBuilder): + def thing_lookup(self, rels): + accounts = Account._byID([rel._thing2_id for rel in rels], data=True) + for rel in rels: + rel._thing2 = accounts.get(rel._thing2_id) + return rels + + def wrap_items(self, rels): + return [self.wrap(rel) for rel in rels] diff --git a/r2/r2/models/listing.py b/r2/r2/models/listing.py index b81562e1f..e9c2f2502 100644 --- a/r2/r2/models/listing.py +++ b/r2/r2/models/listing.py @@ -26,6 +26,7 @@ from vote import * from report import * from subreddit import DefaultSR, AllSR, Frontpage from pylons import i18n, request, g +from pylons.i18n import _ from r2.lib.wrapped import Wrapped from r2.lib import utils @@ -107,6 +108,156 @@ class ModActionListing(TableListing): pass class WikiRevisionListing(TableListing): pass +class UserListing(TableListing): + type = '' + _class = '' + title = '' + form_title = '' + destination = 'friend' + has_add_form = True + headers = None + + def __init__(self, + builder, + show_jump_to=False, + show_not_found=False, + jump_to_value=None, + addable=True, **kw): + self.addable = addable + self.show_not_found = show_not_found + self.show_jump_to = show_jump_to + self.jump_to_value = jump_to_value + TableListing.__init__(self, builder, **kw) + + @property + def container_name(self): + return c.site._fullname + +class FriendListing(UserListing): + type = 'friend' + + @property + def _class(self): + return '' if not c.user.gold else 'gold-accent rounded' + + @property + def headers(self): + if c.user.gold: + return (_('user'), '', _('note'), _('friendship'), '') + + @property + def form_title(self): + return _('add a friend') + + @property + def container_name(self): + return c.user._fullname + + +class EnemyListing(UserListing): + type = 'enemy' + has_add_form = False + + @property + def title(self): + return _('blocked users') + + @property + def container_name(self): + return c.user._fullname + +class BannedListing(UserListing): + type = 'banned' + + @property + def form_title(self): + return _("ban users") + + @property + def title(self): + return _("users banned from" + " /r/%(subreddit)s") % dict(subreddit=c.site.name) + +class WikiBannedListing(BannedListing): + type = 'wikibanned' + + @property + def form_title(self): + return _("ban wiki contibutors") + + @property + def title(self): + return _("wiki contibutors banned from" + " /r/%(subreddit)s") % dict(subreddit=c.site.name) + +class ContributorListing(UserListing): + type = 'contributor' + + @property + def title(self): + return _("approved submitters for" + " /r/%(subreddit)s") % dict(subreddit=c.site.name) + + @property + def form_title(self): + return _("add approved submitter") + +class WikiMayContributeListing(ContributorListing): + type = 'wikicontributor' + + @property + def title(self): + return _("approved wiki contributors" + " for /r/%(subreddit)s") % dict(subreddit=c.site.name) + + @property + def form_title(self): + return _("add approved wiki contributor") + +class InvitedModListing(UserListing): + type = 'moderator_invite' + form_title = _('invite moderator') + remove_self_title = _('you are a moderator of this subreddit. %(action)s') + + def sort_moderators(self, items): + items = [(item, item.rel.get_permissions()) for item in items] + for item, permissions in items: + if permissions is None or permissions.is_superuser(): + yield item + for item, permissions in items: + if permissions is not None and not permissions.is_superuser(): + yield item + + def get_items(self, **kw): + things, prev, next, bcount, acount = UserListing.get_items(self, **kw) + things = list(self.sort_moderators(things)) + return things, prev, next, bcount, acount + + @property + def title(self): + return _("invited moderators for" + " %(subreddit)s") % dict(subreddit=c.site.name) + +class ModListing(InvitedModListing): + type = 'moderator' + form_title = _('force add moderator') + + @property + def has_add_form(self): + return c.user_is_admin + + @property + def can_remove_self(self): + return c.user_is_loggedin and c.site.is_moderator(c.user) + + @property + def has_invite(self): + return c.user_is_loggedin and c.site.is_moderator_invite(c.user) + + @property + def title(self): + return _("moderators of /r/%(subreddit)s") % dict(subreddit=c.site.name) + class LinkListing(Listing): def __init__(self, *a, **kw): Listing.__init__(self, *a, **kw) diff --git a/r2/r2/public/static/css/reddit.less b/r2/r2/public/static/css/reddit.less index 36fbbf93f..e65d1e0cb 100755 --- a/r2/r2/public/static/css/reddit.less +++ b/r2/r2/public/static/css/reddit.less @@ -8231,6 +8231,16 @@ body.with-listing-chooser { } } +.user-jumped-to { + border-radius: 5px; + -moz-border-radius: 5px; + border: 1px solid #DDF; + display: inline-block; + margin-top: 10px; + padding: 10px 15px; + background-color: #EEF; +} + .submit_text { display: none; max-height: 250px; diff --git a/r2/r2/templates/modlist.html b/r2/r2/templates/modlisting.html similarity index 78% rename from r2/r2/templates/modlist.html rename to r2/r2/templates/modlisting.html index 288d90df3..15daca55a 100644 --- a/r2/r2/templates/modlist.html +++ b/r2/r2/templates/modlisting.html @@ -21,12 +21,12 @@ ############################################################################### <%namespace file="printablebuttons.html" import="ynbutton" /> -<%namespace file="userlist.html" import="add_form, userlist"/> +<%namespace file="userlisting.html" import="add_form, listing"/> <%namespace file="utils.html" import="error_field"/>
%if thing.can_remove_self: - ${ynbutton(op=thing.remove_action, + ${ynbutton(op='unfriend', title=_("leave"), executed=_("you are no longer a moderator"), question=_("stop being a moderator?"), @@ -40,7 +40,7 @@ %endif %if thing.has_invite: - ${ynbutton(op=thing.invite_action, + ${ynbutton(op='accept_moderator_invite', title=_("accept"), executed=_("you are now a moderator. welcome to the team!"), question=_("become a moderator of %s?" % ("/r/" + c.site.name)), @@ -49,16 +49,10 @@ _class=thing.type + ' accept-invite')} %endif - %if thing.can_force_add: - ${add_form(thing.form_title, thing.destination, thing.type, thing.container_name)} - %endif - %if thing.addable: - <%call expr="add_form(thing.invite_form_title, thing.destination, thing.invite_type, thing.container_name, verb=_('invite'))"> + %if thing.addable and thing.has_add_form: + <%call expr="add_form(thing.form_title, thing.destination, thing.type, thing.container_name, verb=_('add'))"> ${error_field("ALREADY_MODERATOR", "name")} %endif - %if thing.editable: - ${userlist(_("pending invitations"), thing.invite_type, thing.invited_user_rows, thing.table_headers)} - %endif - ${userlist(thing.table_title, thing.type, thing.user_rows, thing.table_headers)} + ${listing()}
diff --git a/r2/r2/templates/multiinfobar.html b/r2/r2/templates/multiinfobar.html index 76c1e86ed..eaeb8dc72 100644 --- a/r2/r2/templates/multiinfobar.html +++ b/r2/r2/templates/multiinfobar.html @@ -22,7 +22,7 @@ <%! from r2.lib.strings import strings, Score - from r2.lib.pages import WrappedUser, ModList, UserText + from r2.lib.pages import WrappedUser, UserText %> <%namespace file="utils.html" import="plain_link, thing_timestamp, text_with_links"/> diff --git a/r2/r2/templates/subredditinfobar.html b/r2/r2/templates/subredditinfobar.html index d8d05dbeb..08f9d4874 100644 --- a/r2/r2/templates/subredditinfobar.html +++ b/r2/r2/templates/subredditinfobar.html @@ -22,7 +22,8 @@ <%! from r2.lib.strings import strings, Score - from r2.lib.pages import WrappedUser, ModList, SubscribeButton + from r2.lib.pages import WrappedUser, SubscribeButton + from r2.models.listing import ModListing %> <%namespace file="utils.html" import="plain_link, thing_timestamp, text_with_links"/> @@ -41,7 +42,7 @@ %if thing.sr.moderator:
- ${text_with_links(ModList.remove_self_title % dict(action='(%(action)s)'), + ${text_with_links(ModListing.remove_self_title % dict(action='(%(action)s)'), _sr_path=True, action=dict( ## TRANSLATORS: this label links to the edit moderators page. diff --git a/r2/r2/templates/userlisting.html b/r2/r2/templates/userlisting.html new file mode 100644 index 000000000..14402223d --- /dev/null +++ b/r2/r2/templates/userlisting.html @@ -0,0 +1,138 @@ +## 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 r2.models import Sub + %> +<%namespace file="utils.html" import="error_field, plain_link" /> + +<%def name="add_form(title, dest, add_type, container_name, verb=None)"> + <% from r2.models import ModeratorPermissionSet %> + <% from r2.lib.pages import ModeratorPermissions %> +
+

${title}

+ + + + + %if add_type in ("banned", "wikibanned"): + + + + + + ${_('(will not be visible to user)')} + + %else: + + %endif + %if add_type == "moderator_invite": + ${ModeratorPermissions(None, 'moderator_invite', + ModeratorPermissionSet(all=True), + editable=True, embedded=True)} + + + (${_('change')}) + + %endif + + + ${error_field("USER_DOESNT_EXIST", "name")} + %if caller: + ${caller.body()} + %endif +
+ + +<%def name="listing()"> +
+

+ ${thing.title} +

+ + + %if thing.headers: + + %for header in thing.headers: + + %endfor + + %endif + %if thing.things: + %for item in thing.things: + ${item} + %endfor + %else: + + %endif +
${header}
${_('No items found') if thing.show_not_found else ''}
+
+ +%if thing.nextprev and (thing.prev or thing.next): +

${_("view more:")} + %if thing.prev: + ${plain_link(_("first"), thing.first, _sr_path = (c.site != Sub), rel="nofollow first")} + + ${plain_link(unsafe("‹ " + _("prev")), thing.prev, _sr_path = (c.site != Sub), rel="nofollow prev")} + %endif + %if thing.prev and thing.next: + + %endif + %if thing.next: + ${plain_link(unsafe(_("next") + " ›"), thing.next, _sr_path = (c.site != Sub), rel="nofollow next")} + %endif +

+%endif + + + +
+ %if thing.addable and thing.has_add_form: + ${add_form(thing.form_title, thing.destination, thing.type, thing.container_name)} + %endif + + %if thing.show_jump_to: +

${_('jump to')}

+
+ + + +
+ %endif + +${listing()} + +%if thing.jump_to_value: +

+ ${plain_link(_("show all"), request.path, rel="nofollow")} +

+%endif + +
diff --git a/r2/r2/templates/usertableitem.html b/r2/r2/templates/usertableitem.html index 4d0632d2e..5079e30d1 100644 --- a/r2/r2/templates/usertableitem.html +++ b/r2/r2/templates/usertableitem.html @@ -78,7 +78,7 @@ ${timesince(thing.rel._date)} %elif thing.name == "permissions": - ${thing.rel} + ${thing.permissions} %elif thing.name == "permissionsctl": %if thing.editable: