class GoldController(RedditController): def GET_about(self): return GoldInfoPage( _("gold"), show_sidebar=False, page_classes=["gold-page-ga-tracking"] ).render() def GET_partners(self): self.redirect("/gold/about", code=301) @validate( vuser=VExistingUname("username"), ) def GET_snoovatar(self, vuser): if not vuser or vuser._deleted or not vuser.gold: self.abort404() snoovatar = SnoovatarsByAccount.load(vuser, "snoo") user_is_owner = c.user_is_loggedin and c.user == vuser if not user_is_owner: if not snoovatar or not snoovatar["public"]: self.abort404() return SnoovatarProfilePage( user=vuser, content=Snoovatar( editable=user_is_owner, snoovatar=snoovatar, username=vuser.name, ), ).render()
class GoldController(RedditController): def GET_about(self): return GoldInfoPage(_("gold"), show_sidebar=False).render() def GET_partners(self): return GoldPartnersPage(_("gold partners"), show_sidebar=False).render() @validate( vuser=VExistingUname("username"), ) def GET_snoovatar(self, vuser): if not feature.is_enabled('snoovatars'): self.abort404() if not vuser or vuser._deleted or not vuser.gold: self.abort404() snoovatar = SnoovatarsByAccount.load(vuser, "snoo") user_is_owner = c.user_is_loggedin and c.user == vuser if not user_is_owner: if not snoovatar or not snoovatar["public"]: self.abort404() return SnoovatarProfilePage( user=vuser, content=Snoovatar( editable=user_is_owner, snoovatar=snoovatar, username=vuser.name, ), ).render()
class LiveUpdateController(RedditController): def __before__(self, event): RedditController.__before__(self) if event: try: c.liveupdate_event = LiveUpdateEvent._byID(event) except tdb_cassandra.NotFound: pass if not c.liveupdate_event: self.abort404() if c.user_is_loggedin: c.liveupdate_permissions = \ c.liveupdate_event.get_permissions(c.user) # revoke some permissions from everyone after closing if c.liveupdate_event.state != "live": c.liveupdate_permissions = (c.liveupdate_permissions .without("update") .without("close") ) if c.user_is_admin: c.liveupdate_permissions = ContributorPermissionSet.SUPERUSER else: c.liveupdate_permissions = ContributorPermissionSet.NONE if c.liveupdate_event.banned and not c.liveupdate_permissions: error_page = RedditError( title=_("this thread has been banned"), message="", image="subreddit-banned.png", ) request.environ["usable_error_content"] = error_page.render() self.abort403() if (c.liveupdate_event.nsfw and not c.over18 and request.host != g.media_domain and # embeds are special c.render_style == "html"): return self.intermediate_redirect("/over18", sr_path=False) @require_oauth2_scope("read") @validate( num=VLimit("limit", default=25, max_limit=100), after=VLiveUpdateID("after"), before=VLiveUpdateID("before"), count=VCount("count"), is_embed=VBoolean("is_embed", docs={"is_embed": "(internal use only)"}), style_sr=VSRByName("stylesr"), ) @api_doc( section=api_section.live, uri="/live/{thread}", supports_rss=True, notes=[paginated_listing.doc_note], ) def GET_listing(self, num, after, before, count, is_embed, style_sr): """Get a list of updates posted in this thread. See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update). """ # preemptively record activity for clients that don't send pixel pings. # this won't capture their continued visit, but will at least show a # correct activity count for short lived connections. record_activity(c.liveupdate_event._id) reverse = False if before: reverse = True after = before query = LiveUpdateStream.query([c.liveupdate_event._id], count=num, reverse=reverse) if after: query.column_start = after builder = LiveUpdateBuilder(query=query, skip=True, reverse=reverse, num=num, count=count) listing = pages.LiveUpdateListing(builder) wrapped_listing = listing.listing() if c.user_is_loggedin: report_type = LiveUpdateReportsByAccount.get_report( c.user, c.liveupdate_event) else: report_type = None content = pages.LiveUpdateEventApp( event=c.liveupdate_event, listing=wrapped_listing, show_sidebar=not is_embed, report_type=report_type, ) c.js_preload.set_wrapped( "/live/" + c.liveupdate_event._id + "/about.json", Wrapped(c.liveupdate_event), ) c.js_preload.set_wrapped( "/live/" + c.liveupdate_event._id + ".json", wrapped_listing, ) if not is_embed: return pages.LiveUpdateEventAppPage( content=content, page_classes=['liveupdate-app'], ).render() else: # ensure we're off the cookie domain before allowing embedding if request.host != g.media_domain: abort(404) c.allow_framing = True # interstitial redirects and nsfw settings are funky on the media # domain. just disable nsfw embeds. if c.liveupdate_event.nsfw: embed_page = pages.LiveUpdateEventEmbed( content=pages.LiveUpdateNSFWEmbed(), ) request.environ["usable_error_content"] = embed_page.render() abort(403) embed_page = pages.LiveUpdateEventEmbed( content=content, page_classes=['liveupdate-app'], ) if style_sr and getattr(style_sr, "type", "private") != "private": c.can_apply_styles = True c.allow_styles = True embed_page.subreddit_stylesheet_url = \ Reddit.get_subreddit_stylesheet_url(style_sr) return embed_page.render() @require_oauth2_scope("read") @api_doc( section=api_section.live, uri="/live/{thread}/updates/{update_id}", ) def GET_focus(self, target): """Get details about a specific update in a live thread.""" try: target = uuid.UUID(target) except (TypeError, ValueError): self.abort404() try: update = LiveUpdateStream.get_update(c.liveupdate_event, target) except tdb_cassandra.NotFound: self.abort404() if update.deleted: self.abort404() query = FocusQuery([update]) builder = LiveUpdateBuilder( query=query, skip=True, reverse=True, num=1, count=0) listing = pages.LiveUpdateListing(builder) wrapped_listing = listing.listing() c.js_preload.set_wrapped( "/live/" + c.liveupdate_event._id + ".json", wrapped_listing, ) content = pages.LiveUpdateFocusApp( event=c.liveupdate_event, listing=wrapped_listing, ) return pages.LiveUpdateEventFocusPage( content=content, focused_update=update, page_classes=["liveupdate-focus"], ).render() @require_oauth2_scope("read") @api_doc( section=api_section.live, uri="/live/{thread}/about", ) def GET_about(self): """Get some basic information about the live thread. See also: [/api/live/*thread*/edit](#POST_api_live_{thread}_edit). """ if not is_api(): self.abort404() content = Wrapped(c.liveupdate_event) return pages.LiveUpdateEventPage(content=content).render() @require_oauth2_scope("read") @base_listing @api_doc( section=api_section.live, uri="/live/{thread}/discussions", supports_rss=True, ) def GET_discussions(self, num, after, reverse, count): """Get a list of reddit submissions linking to this thread.""" builder = url_links_builder( url="/live/" + c.liveupdate_event._id, num=num, after=after, reverse=reverse, count=count, ) listing = LinkListing(builder).listing() return pages.LiveUpdateEventPage( content=listing, ).render() def GET_edit(self): if not (c.liveupdate_permissions.allow("settings") or c.liveupdate_permissions.allow("close")): abort(403) return pages.LiveUpdateEventPage( content=pages.LiveUpdateEventConfiguration(), ).render() @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("settings"), VModhash(), **EVENT_CONFIGURATION_VALIDATORS ) @api_doc( section=api_section.live, ) def POST_edit(self, form, jquery, title, description, resources, nsfw): """Configure the thread. Requires the `settings` permission for this thread. See also: [/live/*thread*/about.json](#GET_live_{thread}_about.json). """ if not is_event_configuration_valid(form): return changes = {} if title != c.liveupdate_event.title: changes["title"] = title if description != c.liveupdate_event.description: changes["description"] = description changes["description_html"] = safemarkdown(description, nofollow=True) or "" if resources != c.liveupdate_event.resources: changes["resources"] = resources changes["resources_html"] = safemarkdown(resources, nofollow=True) or "" if nsfw != c.liveupdate_event.nsfw: changes["nsfw"] = nsfw if changes: _broadcast(type="settings", payload=changes) c.liveupdate_event.title = title c.liveupdate_event.description = description c.liveupdate_event.resources = resources c.liveupdate_event.nsfw = nsfw c.liveupdate_event._commit() amqp.add_item("liveupdate_event_edited", json.dumps({ "event_fullname": c.liveupdate_event._fullname, "editor_fullname": c.user._fullname, })) form.set_html(".status", _("saved")) form.refresh() # TODO: pass listing params on @require_oauth2_scope("read") @api_doc( section=api_section.live, uri="/live/{thread}/contributors", ) def GET_contributors(self): """Get a list of users that contribute to this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor), and [/api/live/*thread*/rm_contributor] (#POST_api_live_{thread}_rm_contributor). """ editable = c.liveupdate_permissions.allow("manage") content = [pages.LinkBackToLiveUpdate()] contributors = c.liveupdate_event.contributors invites = LiveUpdateContributorInvitesByEvent.get_all(c.liveupdate_event) contributor_builder = LiveUpdateContributorBuilder( c.liveupdate_event, contributors, editable) contributor_listing = pages.LiveUpdateContributorListing( c.liveupdate_event, contributor_builder, has_invite=c.user_is_loggedin and c.user._id in invites, is_contributor=c.user_is_loggedin and c.user._id in contributors, ).listing() content.append(contributor_listing) if editable: invite_builder = LiveUpdateInvitedContributorBuilder( c.liveupdate_event, invites, editable) invite_listing = pages.LiveUpdateInvitedContributorListing( c.liveupdate_event, invite_builder, editable=editable, ).listing() content.append(invite_listing) return pages.LiveUpdateEventPage( content=PaneStack(content), ).render() @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VExistingUname("name"), type_and_perms=VLiveUpdatePermissions("type", "permissions"), ) @api_doc( section=api_section.live, ) def POST_invite_contributor(self, form, jquery, user, type_and_perms): """Invite another user to contribute to the thread. Requires the `manage` permission for this thread. If the recipient accepts the invite, they will be granted the permissions specified. See also: [/api/live/*thread*/accept_contributor_invite] (#POST_api_live_{thread}_accept_contributor_invite), and [/api/live/*thread*/rm_contributor_invite] (#POST_api_live_{thread}_rm_contributor_invite). """ if form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER): return if form.has_errors("type", errors.INVALID_PERMISSION_TYPE): return if form.has_errors("permissions", errors.INVALID_PERMISSIONS): return type, permissions = type_and_perms invites = LiveUpdateContributorInvitesByEvent.get_all(c.liveupdate_event) if user._id in invites or user._id in c.liveupdate_event.contributors: c.errors.add(errors.LIVEUPDATE_ALREADY_CONTRIBUTOR, field="name") form.has_errors("name", errors.LIVEUPDATE_ALREADY_CONTRIBUTOR) return if len(invites) >= g.liveupdate_invite_quota: c.errors.add(errors.LIVEUPDATE_TOO_MANY_INVITES, field="name") form.has_errors("name", errors.LIVEUPDATE_TOO_MANY_INVITES) return LiveUpdateContributorInvitesByEvent.create( c.liveupdate_event, user, permissions) queries.add_contributor(c.liveupdate_event, user) # TODO: make this i18n-friendly when we have such a system for PMs send_system_message( user, subject="invitation to contribute to " + c.liveupdate_event.title, body=INVITE_MESSAGE % { "title": c.liveupdate_event.title, "url": "/live/" + c.liveupdate_event._id, }, ) amqp.add_item("new_liveupdate_contributor", json.dumps({ "event_fullname": c.liveupdate_event._fullname, "inviter_fullname": c.user._fullname, "invitee_fullname": user._fullname, })) # add the user to the table contributor = LiveUpdateContributor(user, permissions) user_row = pages.InvitedLiveUpdateContributorTableItem( contributor, c.liveupdate_event, editable=True) jquery(".liveupdate_contributor_invite-table").show( ).find("table").insert_table_rows(user_row) @require_oauth2_scope("livemanage") @validatedForm( VUser(), VModhash(), ) @api_doc( section=api_section.live, ) def POST_leave_contributor(self, form, jquery): """Abdicate contributorship of the thread. See also: [/api/live/*thread*/accept_contributor_invite] (#POST_api_live_{thread}_accept_contributor_invite), and [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor). """ c.liveupdate_event.remove_contributor(c.user) queries.remove_contributor(c.liveupdate_event, c.user) @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VByName("id", thing_cls=Account), ) @api_doc( section=api_section.live, ) def POST_rm_contributor_invite(self, form, jquery, user): """Revoke an outstanding contributor invite. Requires the `manage` permission for this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor). """ LiveUpdateContributorInvitesByEvent.remove( c.liveupdate_event, user) queries.remove_contributor(c.liveupdate_event, user) @require_oauth2_scope("livemanage") @validatedForm( VUser(), VModhash(), ) @api_doc( section=api_section.live, ) def POST_accept_contributor_invite(self, form, jquery): """Accept a pending invitation to contribute to the thread. See also: [/api/live/*thread*/leave_contributor] (#POST_api_live_{thread}_leave_contributor). """ try: permissions = LiveUpdateContributorInvitesByEvent.get( c.liveupdate_event, c.user) except InviteNotFoundError: c.errors.add(errors.LIVEUPDATE_NO_INVITE_FOUND) form.set_error(errors.LIVEUPDATE_NO_INVITE_FOUND, None) return LiveUpdateContributorInvitesByEvent.remove( c.liveupdate_event, c.user) c.liveupdate_event.add_contributor(c.user, permissions) jquery.refresh() @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VExistingUname("name"), type_and_perms=VLiveUpdatePermissions("type", "permissions"), ) @api_doc( section=api_section.live, ) def POST_set_contributor_permissions(self, form, jquery, user, type_and_perms): """Change a contributor or contributor invite's permissions. Requires the `manage` permission for this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor) and [/api/live/*thread*/rm_contributor] (#POST_api_live_{thread}_rm_contributor). """ if form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER): return if form.has_errors("type", errors.INVALID_PERMISSION_TYPE): return if form.has_errors("permissions", errors.INVALID_PERMISSIONS): return type, permissions = type_and_perms if type == "liveupdate_contributor": if user._id not in c.liveupdate_event.contributors: c.errors.add(errors.LIVEUPDATE_NOT_CONTRIBUTOR, field="user") form.has_errors("user", errors.LIVEUPDATE_NOT_CONTRIBUTOR) return c.liveupdate_event.update_contributor_permissions(user, permissions) elif type == "liveupdate_contributor_invite": try: LiveUpdateContributorInvitesByEvent.get( c.liveupdate_event, user) except InviteNotFoundError: c.errors.add(errors.LIVEUPDATE_NO_INVITE_FOUND, field="user") form.has_errors("user", errors.LIVEUPDATE_NO_INVITE_FOUND) return else: LiveUpdateContributorInvitesByEvent.update_invite_permissions( c.liveupdate_event, user, permissions) row = form.closest("tr") editor = row.find(".permissions").data("PermissionEditor") editor.onCommit(permissions.dumps()) @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VByName("id", thing_cls=Account), ) @api_doc( section=api_section.live, ) def POST_rm_contributor(self, form, jquery, user): """Revoke another user's contributorship. Requires the `manage` permission for this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor). """ c.liveupdate_event.remove_contributor(user) queries.remove_contributor(c.liveupdate_event, user) @require_oauth2_scope("submit") @validatedForm( VLiveUpdateContributorWithPermission("update"), VModhash(), text=VMarkdownLength("body", max_length=4096), ) @api_doc( section=api_section.live, ) def POST_update(self, form, jquery, text): """Post an update to the thread. Requires the `update` permission for this thread. See also: [/api/live/*thread*/strike_update] (#POST_api_live_{thread}_strike_update), and [/api/live/*thread*/delete_update] (#POST_api_live_{thread}_delete_update). """ if form.has_errors("body", errors.NO_TEXT, errors.TOO_LONG): return # create and store the new update update = LiveUpdate(data={ "author_id": c.user._id, "body": text, "_spam": c.user._spam, }) hooks.get_hook("liveupdate.update").call(update=update) LiveUpdateStream.add_update(c.liveupdate_event, update) # tell the world about our new update builder = LiveUpdateBuilder(None) wrapped = builder.wrap_items([update])[0] rendered = wrapped.render(style="api") _broadcast(type="update", payload=rendered) amqp.add_item("new_liveupdate_update", json.dumps({ "event_fullname": c.liveupdate_event._fullname, "author_fullname": c.user._fullname, "liveupdate_id": str(update._id), "body": text, })) liveupdate_events.update_event(update, context=c, request=request) # reset the submission form t = form.find("textarea") t.attr('rows', 3).html("").val("") @require_oauth2_scope("edit") @validatedForm( VModhash(), update=VLiveUpdate("id"), ) @api_doc( section=api_section.live, ) def POST_delete_update(self, form, jquery, update): """Delete an update from the thread. Requires that specified update must have been authored by the user or that you have the `edit` permission for this thread. See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update). """ if form.has_errors("id", errors.NO_THING_ID): return if not (c.liveupdate_permissions.allow("edit") or (c.user_is_loggedin and update.author_id == c.user._id)): abort(403) update.deleted = True LiveUpdateStream.add_update(c.liveupdate_event, update) liveupdate_events.update_event(update, context=c, request=request) _broadcast(type="delete", payload=update._fullname) @require_oauth2_scope("edit") @validatedForm( VModhash(), update=VLiveUpdate("id"), ) @api_doc( section=api_section.live, ) def POST_strike_update(self, form, jquery, update): """Strike (mark incorrect and cross out) the content of an update. Requires that specified update must have been authored by the user or that you have the `edit` permission for this thread. See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update). """ if form.has_errors("id", errors.NO_THING_ID): return if not (c.liveupdate_permissions.allow("edit") or (c.user_is_loggedin and update.author_id == c.user._id)): abort(403) update.stricken = True LiveUpdateStream.add_update(c.liveupdate_event, update) liveupdate_events.update_event( update, stricken=True, context=c, request=request ) _broadcast(type="strike", payload=update._fullname) @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("close"), VModhash(), ) @api_doc( section=api_section.live, ) def POST_close_thread(self, form, jquery): """Permanently close the thread, disallowing future updates. Requires the `close` permission for this thread. """ close_event(c.liveupdate_event) liveupdate_events.close_event(context=c, request=request) form.refresh() @require_oauth2_scope("report") @validatedForm( VUser(), VModhash(), report_type=VOneOf("type", pages.REPORT_TYPES), ) @api_doc( section=api_section.live, ) def POST_report(self, form, jquery, report_type): """Report the thread for violating the rules of reddit.""" if form.has_errors("type", errors.INVALID_OPTION): return if c.user._spam or c.user.ignorereports: return already_reported = LiveUpdateReportsByAccount.get_report( c.user, c.liveupdate_event) if already_reported: self.abort403() LiveUpdateReportsByAccount.create( c.user, c.liveupdate_event, type=report_type) queries.report_event(c.liveupdate_event) liveupdate_events.report_event( report_type, context=c, request=request ) amqp.add_item("new_liveupdate_report", json.dumps({ "event_fullname": c.liveupdate_event._fullname, "reporter_fullname": c.user._fullname, "reason": report_type, })) try: default_subreddit = Subreddit._by_name(g.default_sr) except NotFound: pass else: not_yet_reported = g.ratelimitcache.add( "rl:lu_reported_" + str(c.liveupdate_event._id), 1, time=3600) if not_yet_reported: send_system_message( default_subreddit, subject="live thread reported", body=REPORTED_MESSAGE % { "title": c.liveupdate_event.title, "url": "/live/" + c.liveupdate_event._id, "reason": pages.REPORT_TYPES[report_type], }, ) @validatedForm( VAdmin(), VModhash(), ) def POST_approve(self, form, jquery): c.liveupdate_event.banned = False c.liveupdate_event._commit() queries.unreport_event(c.liveupdate_event) liveupdate_events.ban_event(context=c, request=request) @validatedForm( VAdmin(), VModhash(), ) def POST_ban(self, form, jquery): c.liveupdate_event.banned = True c.liveupdate_event.banned_by = c.user.name c.liveupdate_event._commit() queries.unreport_event(c.liveupdate_event) liveupdate_events.ban_event(context=c, request=request)
class WikiApiController(WikiController): @require_oauth2_scope("wikiedit") @validate(VModhash(), pageandprevious=VWikiPageRevise(('page', 'previous'), restricted=True), content=nop(('content')), page_name=VWikiPageName('page'), reason=VPrintable('reason', 256, empty_error=None)) @api_doc(api_section.wiki, uri='/api/wiki/edit', uses_site=True) def POST_wiki_edit(self, pageandprevious, content, page_name, reason): """Edit a wiki `page`""" page, previous = pageandprevious if not page: error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) if not c.user._spam: page = WikiPage.create(c.site, page_name) if c.user._spam: error = _("You are doing that too much, please try again later.") self.handle_error(415, 'SPECIAL_ERRORS', special_errors=[error]) renderer = RENDERERS_BY_PAGE.get(page.name, 'wiki') if renderer in ('wiki', 'reddit'): content = VMarkdown(('content'), renderer=renderer).run(content) # Use the raw POST value as we need to tell the difference between # None/Undefined and an empty string. The validators use a default # value with both of those cases and would need to be changed. # In order to avoid breaking functionality, this was done instead. previous = previous._id if previous else request.POST.get('previous') try: # special validation methods if page.name == 'config/stylesheet': css_errors, parsed = c.site.parse_css(content, verify=False) if g.css_killswitch: self.handle_error(403, 'STYLESHEET_EDIT_DENIED') if css_errors: error_items = [CssError(x).message for x in css_errors] self.handle_error(415, 'SPECIAL_ERRORS', special_errors=error_items) elif page.name == "config/automoderator": try: rules = Ruleset(content) except ValueError as e: error_items = [e.message] self.handle_error(415, "SPECIAL_ERRORS", special_errors=error_items) # special saving methods if page.name == "config/stylesheet": c.site.change_css(content, parsed, previous, reason=reason) else: try: page.revise(content, previous, c.user._id36, reason=reason) except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special and page.name in ATTRIBUTE_BY_PAGE: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) c.site._commit() if page.special or c.is_wiki_mod: description = modactions.get(page.name, 'Page %s edited' % page.name) ModAction.create(c.site, c.user, "wikirevise", details=description, description=reason) except ConflictException as e: self.handle_error(409, 'EDIT_CONFLICT', newcontent=e.new, newrevision=page.revision, diffcontent=e.htmldiff) return json.dumps({}) @require_oauth2_scope("modwiki") @validate(VModhash(), VWikiModerator(), page=VWikiPage('page'), act=VOneOf('act', ('del', 'add')), user=VExistingUname('username')) @api_doc(api_section.wiki, uri='/api/wiki/alloweditor/{act}', uses_site=True, uri_variants=[ '/api/wiki/alloweditor/%s' % act for act in ('del', 'add') ]) def POST_wiki_allow_editor(self, act, page, user): """Allow/deny `username` to edit this wiki `page`""" if not user: self.handle_error(404, 'UNKNOWN_USER') elif act == 'del': page.remove_editor(user._id36) elif act == 'add': page.add_editor(user._id36) else: self.handle_error(400, 'INVALID_ACTION') return json.dumps({}) @validate( VModhash(), VAdmin(), pv=VWikiPageAndVersion(('page', 'revision')), deleted=VBoolean('deleted'), ) def POST_wiki_revision_delete(self, pv, deleted): page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') if deleted and page.revision == str(revision._id): self.handle_error(400, 'REVISION_IS_CURRENT') revision.admin_deleted = deleted revision._commit() return json.dumps({'status': revision.admin_deleted}) @require_oauth2_scope("modwiki") @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/hide', uses_site=True) def POST_wiki_revision_hide(self, pv): """Toggle the public visibility of a wiki page revision""" page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') return json.dumps({'status': revision.toggle_hide()}) @require_oauth2_scope("modwiki") @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/revert', uses_site=True) def POST_wiki_revision_revert(self, pv): """Revert a wiki `page` to `revision`""" page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') content = revision.content reason = 'reverted back %s' % timesince(revision.date) if page.name == 'config/stylesheet': css_errors, parsed = c.site.parse_css(content) if css_errors: self.handle_error(403, 'INVALID_CSS') c.site.change_css(content, parsed, prev=None, reason=reason, force=True) else: try: page.revise(content, author=c.user._id36, reason=reason, force=True) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) c.site._commit() except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) return json.dumps({}) def pre(self): WikiController.pre(self) c.render_style = 'api' set_extension(request.environ, 'json')
class PromoteController(ListingController): where = 'promoted' render_cls = PromotePage @property def title_text(self): return _('promoted by you') @classmethod @memoize('live_by_subreddit', time=300) def live_by_subreddit(cls, sr): if sr == Frontpage: sr_id = '' else: sr_id = sr._id r = LiveAdWeights.get([sr_id]) return [i.link for i in r[sr_id]] @classmethod @memoize('subreddits_with_promos', time=3600) def subreddits_with_promos(cls): sr_ids = LiveAdWeights.get_live_subreddits() srs = Subreddit._byID(sr_ids, return_dict=False) sr_names = sorted([sr.name for sr in srs], key=lambda s: s.lower()) return sr_names @property def menus(self): filters = [ NamedButton('all_promos', dest=''), NamedButton('future_promos'), NamedButton('unpaid_promos'), NamedButton('rejected_promos'), NamedButton('pending_promos'), NamedButton('live_promos'), ] menus = [ NavMenu(filters, base_path='/promoted', title='show', type='lightdrop') ] if self.sort == 'live_promos' and c.user_is_sponsor: sr_names = self.subreddits_with_promos() buttons = [NavButton(name, name) for name in sr_names] frontbutton = NavButton('FRONTPAGE', Frontpage.name, aliases=[ '/promoted/live_promos/%s' % urllib.quote(Frontpage.name) ]) buttons.insert(0, frontbutton) buttons.insert(0, NavButton('all', '')) menus.append( NavMenu(buttons, base_path='/promoted/live_promos', title='subreddit', type='lightdrop')) return menus def keep_fn(self): def keep(item): if item.promoted and not item._deleted: return True else: return False return keep def query(self): if c.user_is_sponsor: if self.sort == "future_promos": return queries.get_all_unapproved_links() elif self.sort == "pending_promos": return queries.get_all_accepted_links() elif self.sort == "unpaid_promos": return queries.get_all_unpaid_links() elif self.sort == "rejected_promos": return queries.get_all_rejected_links() elif self.sort == "live_promos" and self.sr: return self.live_by_subreddit(self.sr) elif self.sort == 'live_promos': return queries.get_all_live_links() elif self.sort == 'underdelivered': q = queries.get_underdelivered_campaigns() campaigns = PromoCampaign._by_fullname(list(q), data=True, return_dict=False) link_ids = [camp.link_id for camp in campaigns] return [Link._fullname_from_id36(to36(id)) for id in link_ids] return queries.get_all_promoted_links() else: if self.sort == "future_promos": return queries.get_unapproved_links(c.user._id) elif self.sort == "pending_promos": return queries.get_accepted_links(c.user._id) elif self.sort == "unpaid_promos": return queries.get_unpaid_links(c.user._id) elif self.sort == "rejected_promos": return queries.get_rejected_links(c.user._id) elif self.sort == "live_promos": return queries.get_live_links(c.user._id) return queries.get_promoted_links(c.user._id) @validate(VSponsor(), sr=nop('sr')) def GET_listing(self, sr=None, sort="", **env): if not c.user_is_loggedin or not c.user.email_verified: return self.redirect("/ad_inq") self.sort = sort self.sr = None if sr and sr == Frontpage.name: self.sr = Frontpage elif sr: try: self.sr = Subreddit._by_name(sr) except NotFound: pass return ListingController.GET_listing(self, **env) GET_index = GET_listing @validate(VSponsor()) def GET_new_promo(self): return PromotePage('content', content=PromoteLinkNew()).render() @validate(VSponsor('link'), link=VLink('link')) def GET_edit_promo(self, link): if not link or link.promoted is None: return self.abort404() rendered = wrap_links(link, wrapper=promote.sponsor_wrapper, skip=False) form = PromoteLinkForm(link, rendered) page = PromotePage('new_promo', content=form) return page.render() # admin only because the route might change @validate(VSponsorAdmin('campaign'), campaign=VPromoCampaign('campaign')) def GET_edit_promo_campaign(self, campaign): if not campaign: return self.abort404() link = Link._byID(campaign.link_id) return self.redirect(promote.promo_edit_url(link)) @json_validate(sr=VSubmitSR('sr', promotion=True), start=VDate('startdate'), end=VDate('enddate')) def GET_check_inventory(self, responder, sr, start, end): sr = sr or Frontpage available_by_datestr = inventory.get_available_pageviews(sr, start, end, datestr=True) return {'inventory': available_by_datestr} @validate(VSponsor(), dates=VDateRange(["startdate", "enddate"], max_range=timedelta(days=28), required=False)) def GET_graph(self, dates): start, end, bad_dates = _check_dates(dates) return PromotePage("graph", content=Promote_Graph( start, end, bad_dates=bad_dates)).render() @validate(VSponsorAdmin(), dates=VDateRange(["startdate", "enddate"], max_range=timedelta(days=28), required=False)) def GET_admingraph(self, dates): start, end, bad_dates = _check_dates(dates) content = Promote_Graph(start, end, bad_dates=bad_dates, admin_view=True) if c.render_style == 'csv': return content.as_csv() return PromotePage("admingraph", content=content).render() # ## POST controllers below @validatedForm(VSponsorAdmin(), link=VLink("link_id"), campaign=VPromoCampaign("campaign_id36")) def POST_freebie(self, form, jquery, link, campaign): if campaign_has_oversold_error(form, campaign): form.set_html(".freebie", "target oversold, can't freebie") return if promote.is_promo(link) and campaign: promote.free_campaign(link, campaign, c.user) form.redirect(promote.promo_edit_url(link)) @validatedForm(VSponsorAdmin(), link=VByName("link"), note=nop("note")) def POST_promote_note(self, form, jquery, link, note): if promote.is_promo(link): text = PromotionLog.add(link, note) form.find(".notes").children(":last").after("<p>" + text + "</p>") @noresponse(VSponsorAdmin(), thing=VByName('id')) def POST_promote(self, thing): if promote.is_promo(thing): promote.accept_promotion(thing) @noresponse(VSponsorAdmin(), thing=VByName('id'), reason=nop("reason")) def POST_unpromote(self, thing, reason): if promote.is_promo(thing): promote.reject_promotion(thing, reason=reason) @validate(VSponsorAdmin(), link=VLink("link"), campaign=VPromoCampaign("campaign")) def GET_refund(self, link, campaign): if campaign.link_id != link._id: return self.abort404() content = RefundPage(link, campaign) return Reddit("refund", content=content, show_sidebar=False).render() @validatedForm(VSponsorAdmin(), link=VLink('link'), campaign=VPromoCampaign('campaign')) def POST_refund_campaign(self, form, jquery, link, campaign): billable_impressions = promote.get_billable_impressions(campaign) billable_amount = promote.get_billable_amount(campaign, billable_impressions) refund_amount = campaign.bid - billable_amount if refund_amount > 0: promote.refund_campaign(link, campaign, billable_amount) form.set_html('.status', _('refund succeeded')) else: form.set_html('.status', _('refund not needed')) @validatedForm(VSponsor('link_id'), VModhash(), VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'), VShamedDomain('url'), l=VLink('link_id'), title=VTitle('title'), url=VUrl('url', allow_self=False, lookup=False), ip=ValidIP(), disable_comments=VBoolean("disable_comments"), media_width=VInt("media-width", min=0), media_height=VInt("media-height", min=0), media_embed=VLength("media-embed", 1000), media_override=VBoolean("media-override"), domain_override=VLength("domain", 100)) def POST_edit_promo(self, form, jquery, ip, l, title, url, disable_comments, media_height, media_width, media_embed, media_override, domain_override): should_ratelimit = False if not c.user_is_sponsor: should_ratelimit = True if not should_ratelimit: c.errors.remove((errors.RATELIMIT, 'ratelimit')) # check for shame banned domains if form.has_errors("url", errors.DOMAIN_BANNED): g.stats.simple_event('spam.shame.link') return # demangle URL in canonical way if url: if isinstance(url, (unicode, str)): form.set_inputs(url=url) elif isinstance(url, tuple) or isinstance(url[0], Link): # there's already one or more links with this URL, but # we're allowing mutliple submissions, so we really just # want the URL url = url[0].url # users can change the disable_comments on promoted links if ((not l or not promote.is_promoted(l)) and (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG) or form.has_errors('url', errors.NO_URL, errors.BAD_URL) or jquery.has_errors('ratelimit', errors.RATELIMIT))): return if not l: l = promote.new_promotion(title, url, c.user, ip) elif promote.is_promo(l): changed = False # live items can only be changed by a sponsor, and also # pay the cost of de-approving the link trusted = c.user_is_sponsor or c.user.trusted_sponsor if not promote.is_promoted(l) or trusted: if title and title != l.title: l.title = title changed = not trusted if url and url != l.url: l.url = url changed = not trusted # only trips if the title and url are changed by a non-sponsor if changed and not promote.is_unpaid(l): promote.unapprove_promotion(l) if trusted and promote.is_unapproved(l): promote.accept_promotion(l) # comment disabling is free to be changed any time. l.disable_comments = disable_comments if c.user_is_sponsor or c.user.trusted_sponsor: if media_embed and media_width and media_height: l.media_object = dict(height=media_height, width=media_width, content=media_embed, type='custom') else: l.media_object = None l.media_override = media_override if getattr(l, "domain_override", False) or domain_override: l.domain_override = domain_override l._commit() form.redirect(promote.promo_edit_url(l)) @validate(VSponsorAdmin()) def GET_roadblock(self): return PromotePage('content', content=Roadblocks()).render() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), sr=VSubmitSR('sr', promotion=True)) def POST_add_roadblock(self, form, jquery, dates, sr): if (form.has_errors('startdate', errors.BAD_DATE, errors.BAD_FUTURE_DATE) or form.has_errors('enddate', errors.BAD_DATE, errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)): return if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): return if dates and sr: sd, ed = dates PromotedLinkRoadblock.add(sr, sd, ed) jquery.refresh() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), sr=VSubmitSR('sr', promotion=True)) def POST_rm_roadblock(self, form, jquery, dates, sr): if dates and sr: sd, ed = dates PromotedLinkRoadblock.remove(sr, sd, ed) jquery.refresh() @validatedForm(VSponsor('link_id'), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), link=VLink('link_id'), bid=VBid('bid', min=0, max=g.max_promote_bid, coerce=False, error=errors.BAD_BID), sr=VSubmitSR('sr', promotion=True), campaign_id36=nop("campaign_id36"), targeting=VLength("targeting", 10)) def POST_edit_campaign(self, form, jquery, link, campaign_id36, dates, bid, sr, targeting): if not link: return start, end = dates or (None, None) author = Account._byID(link.author_id, data=True) cpm = author.cpm_selfserve_pennies if (start and end and not promote.is_accepted(link) and not c.user_is_sponsor): # if the ad is not approved already, ensure the start date # is at least 2 days in the future start = start.date() end = end.date() now = promote.promo_datetime_now() future = make_offset_date(now, g.min_promote_future, business_days=True) if start < future.date(): c.errors.add(errors.BAD_FUTURE_DATE, msg_params=dict(day=g.min_promote_future), field="startdate") if (form.has_errors('startdate', errors.BAD_DATE, errors.BAD_FUTURE_DATE) or form.has_errors('enddate', errors.BAD_DATE, errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)): return # Limit the number of PromoCampaigns a Link can have # Note that the front end should prevent the user from getting # this far existing_campaigns = list(PromoCampaign._by_link(link._id)) if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK: c.errors.add(errors.TOO_MANY_CAMPAIGNS, msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK}, field='title') form.has_errors('title', errors.TOO_MANY_CAMPAIGNS) return if form.has_errors('bid', errors.BAD_BID): return if campaign_id36: # you cannot edit the bid of a live ad unless it's a freebie try: campaign = PromoCampaign._byID36(campaign_id36) if (bid != campaign.bid and campaign.start_date < datetime.now(g.tz) and not campaign.is_freebie()): c.errors.add(errors.BID_LIVE, field='bid') form.has_errors('bid', errors.BID_LIVE) return except NotFound: pass min_bid = 0 if c.user_is_sponsor else g.min_promote_bid if bid is None or bid < min_bid: c.errors.add(errors.BAD_BID, field='bid', msg_params={ 'min': min_bid, 'max': g.max_promote_bid }) form.has_errors('bid', errors.BAD_BID) return if targeting == 'one': if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): # checking to get the error set in the form, but we can't # check for rate-limiting if there's no subreddit return roadblock = PromotedLinkRoadblock.is_roadblocked(sr, start, end) if roadblock and not c.user_is_sponsor: msg_params = { "start": roadblock[0].strftime('%m/%d/%Y'), "end": roadblock[1].strftime('%m/%d/%Y') } c.errors.add(errors.OVERSOLD, field='sr', msg_params=msg_params) form.has_errors('sr', errors.OVERSOLD) return elif targeting == 'none': sr = None # Check inventory campaign_id = campaign._id if campaign_id36 else None if has_oversold_error(form, campaign_id, start, end, bid, cpm, sr): return if campaign_id36 is not None: campaign = PromoCampaign._byID36(campaign_id36) promote.edit_campaign(link, campaign, dates, bid, cpm, sr) r = promote.get_renderable_campaigns(link, campaign) jquery.update_campaign(r.campaign_id36, r.start_date, r.end_date, r.duration, r.bid, r.spent, r.cpm, r.sr, r.status) else: campaign = promote.new_campaign(link, dates, bid, cpm, sr) r = promote.get_renderable_campaigns(link, campaign) jquery.new_campaign(r.campaign_id36, r.start_date, r.end_date, r.duration, r.bid, r.spent, r.cpm, r.sr, r.status) @validatedForm(VSponsor('link_id'), VModhash(), l=VLink('link_id'), campaign=VPromoCampaign("campaign_id36")) def POST_delete_campaign(self, form, jquery, l, campaign): if l and campaign: promote.delete_campaign(l, campaign) @validatedForm(VSponsor('container'), VModhash(), user=VExistingUname('name'), thing=VByName('container')) def POST_traffic_viewer(self, form, jquery, user, thing): """ Adds a user to the list of users allowed to view a promoted link's traffic page. """ if not form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER): form.set_inputs(name="") form.set_html(".status:first", _("added")) if promote.add_traffic_viewer(thing, user): user_row = TrafficViewerList(thing).user_row( 'traffic_viewer', user) jquery(".traffic_viewer-table").show().find( "table").insert_table_rows(user_row) # send the user a message msg = user_added_messages['traffic']['pm']['msg'] subj = user_added_messages['traffic']['pm']['subject'] if msg and subj: d = dict(url=thing.make_permalink_slow(), traffic_url=promote.promo_traffic_url(thing), title=thing.title) msg = msg % d item, inbox_rel = Message._new(c.user, user, subj, msg, request.ip) queries.new_message(item, inbox_rel) @validatedForm(VSponsor('container'), VModhash(), iuser=VByName('id'), thing=VByName('container')) def POST_rm_traffic_viewer(self, form, jquery, iuser, thing): if thing and iuser: promote.rm_traffic_viewer(thing, iuser) @validatedForm( VSponsor('link'), link=VByName("link"), campaign=VPromoCampaign("campaign"), customer_id=VInt("customer_id", min=0), pay_id=VInt("account", min=0), edit=VBoolean("edit"), address=ValidAddress([ "firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber" ], allowed_countries=g.allowed_pay_countries), creditcard=ValidCard(["cardNumber", "expirationDate", "cardCode"])) def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id, edit, address, creditcard): # Check inventory if campaign_has_oversold_error(form, campaign): return address_modified = not pay_id or edit form_has_errors = False if address_modified: if (form.has_errors([ "firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber" ], errors.BAD_ADDRESS) or form.has_errors( ["cardNumber", "expirationDate", "cardCode"], errors.BAD_CARD)): form_has_errors = True elif g.authorizenetapi: pay_id = edit_profile(c.user, address, creditcard, pay_id) else: pay_id = 1 # if link is in use or finished, don't make a change if pay_id and not form_has_errors: # valid bid and created or existing bid id. # check if already a transaction if g.authorizenetapi: success, reason = promote.auth_campaign( link, campaign, c.user, pay_id) else: success = True if success: form.redirect(promote.promo_edit_url(link)) else: form.set_html( ".status", reason or _("failed to authenticate card. sorry.")) @validate(VSponsor("link"), link=VLink("link"), campaign=VPromoCampaign("campaign")) def GET_pay(self, link, campaign): # no need for admins to play in the credit card area if c.user_is_loggedin and c.user._id != link.author_id: return self.abort404() if not campaign.link_id == link._id: return self.abort404() if g.authorizenetapi: data = get_account_info(c.user) content = PaymentForm(link, campaign, customer_id=data.customerProfileId, profiles=data.paymentProfiles, max_profiles=PROFILE_LIMIT) else: content = None res = LinkInfoPage(link=link, content=content, show_sidebar=False) return res.render() def GET_link_thumb(self, *a, **kw): """ See GET_upload_sr_image for rationale """ return "nothing to see here." @validate(VSponsor("link_id"), link=VByName('link_id'), file=VLength('file', 500 * 1024), img_type=VImageType('img_type')) def POST_link_thumb(self, link=None, file=None, img_type='jpg'): if link and (not promote.is_promoted(link) or c.user_is_sponsor or c.user.trusted_sponsor): errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="") try: # thumnails for promoted links can change and therefore expire force_thumbnail(link, file, file_type=".%s" % img_type) except cssfilter.BadImage: # if the image doesn't clean up nicely, abort errors["IMAGE_ERROR"] = _("bad image") if any(errors.values()): return UploadedImage("", "", "upload", errors=errors, form_id="image-upload").render() else: link._commit() return UploadedImage(_('saved'), thumbnail_url(link), "", errors=errors, form_id="image-upload").render() @validate(VSponsorAdmin(), launchdate=VDate('ondate'), dates=VDateRange(['startdate', 'enddate']), query_type=VOneOf('q', ('started_on', 'between'), default=None)) def GET_admin(self, launchdate=None, dates=None, query_type=None): return PromoAdminTool(query_type=query_type, launchdate=launchdate, start=dates[0], end=dates[1]).render() @validate(VSponsorAdminOrAdminSecret('secret'), start=VDate('startdate'), end=VDate('enddate'), link_text=nop('link_text'), owner=VAccountByName('owner')) def GET_report(self, start, end, link_text=None, owner=None): now = datetime.now(g.tz).replace(hour=0, minute=0, second=0, microsecond=0) end = end or now - timedelta(days=1) start = start or end - timedelta(days=7) links = [] bad_links = [] owner_name = owner.name if owner else '' if owner: promo_weights = PromotionWeights.get_campaigns(start, end, author_id=owner._id) campaign_ids = [pw.promo_idx for pw in promo_weights] campaigns = PromoCampaign._byID(campaign_ids, data=True) link_ids = {camp.link_id for camp in campaigns.itervalues()} links.extend(Link._byID(link_ids, data=True, return_dict=False)) if link_text is not None: id36s = link_text.replace(',', ' ').split() try: links_from_text = Link._byID36(id36s, data=True) except NotFound: links_from_text = {} bad_links = [id36 for id36 in id36s if id36 not in links_from_text] links.extend(links_from_text.values()) content = PromoteReport(links, link_text, owner_name, bad_links, start, end) if c.render_style == 'csv': return content.as_csv() else: return PromotePage('report', content=content).render()
class QrCodeController(RedditController): @validate( meetup=validators.VMeetup("codename"), ) def GET_portal(self, meetup): if meetup.state != "closed": if c.user_is_loggedin: content = pages.MeetupPortal(meetup=meetup) else: content = pages.LoggedOutMeetupPortal(meetup=meetup) else: content = pages.ClosedMeetupPortal(meetup=meetup) return pages.MeatspacePage(content=content, page_classes=["meatspace-portal"]).render() @validate( VUser(), meetup=validators.VMeetup("codename"), ) def GET_configure_badge(self, meetup): if meetup.state not in BADGE_STATES: return redirect_to("/meetup/%s" % str(meetup._id)) content = pages.ConversationStarterSelector(meetup, c.user) return pages.MeatspacePage(content=content).render() @validate( VUser(), meetup=validators.VMeetup("codename"), topic=validators.VConversationStarter("topic"), ) def GET_badge(self, meetup, topic): if meetup.state not in BADGE_STATES: return redirect_to("/meetup/%s" % str(meetup._id)) content = pages.QrCodeBadge(meetup, c.user, topic) return pages.MeatspaceBadgePage(content=content).render() @validate( VUser(), meetup=validators.VMeetup("codename"), ) def GET_mobile_badge(self, meetup): if meetup.state not in BADGE_STATES: return redirect_to("/meetup/%s" % str(meetup._id)) content = pages.MobileQrCodeBadge(meetup, c.user) return content.render() @validate( VUser(), meetup=validators.VMeetup("codename"), other=VExistingUname("user"), connected_with=VExistingUname("connected-with"), code=VInt("code"), ) def GET_connect(self, meetup, other, code, connected_with): if meetup.state not in CONNECT_STATES: self.abort404() content = pages.QrCodeForm( meetup=meetup, other=other, code=code, connected_with=connected_with, ) return pages.MeatspacePage(content=content).render() @validatedForm( VUser(), meetup=validators.VMeetup("codename"), other=VExistingUname("username"), code=VInt("code"), ) def POST_connect(self, form, jquery, meetup, other, code): if meetup.state not in CONNECT_STATES: self.abort403() jquery("body .connection-success").hide() if form.has_errors("username", errors.NO_USER, errors.USER_DOESNT_EXIST): return if c.user == other: c.errors.add(errors.MEETUP_NOT_WITH_SELF, field="username") form.set_error(errors.MEETUP_NOT_WITH_SELF, "username") return expected_code = utils.make_secret_code(meetup, other) if code != expected_code: g.log.warning("%r just tried an invalid code on %r", c.user.name, other.name) c.errors.add(errors.MEETUP_INVALID_CODE, field="code") form.set_error(errors.MEETUP_INVALID_CODE, "code") return models.MeetupConnections._connect(meetup, c.user, other) models.MeetupConnectionsByAccount._connect(meetup, c.user, other) g.stats.simple_event("meetup.connection") form.redirect("/meetup/%s/connect?connected-with=%s" % (meetup._id, other.name)) @validate( VUser(), meetup=validators.VMeetup("codename"), ) def GET_connections(self, meetup): all_connections = models.MeetupConnectionsByAccount._connections( meetup, c.user) connections = [a for a in all_connections if not a._deleted] content = pages.QrCodeConnections( meetup=meetup, connections=connections, ) return pages.MeatspacePage(content=content).render() @validate(meetup=validators.VMeetup("codename")) def GET_connect_shortlink(self, meetup, user, code): if meetup.state not in CONNECT_STATES: self.abort404() params = urllib.urlencode({ "user": user, "code": code, }) return redirect_to("/meetup/%s/connect?%s" % (str(meetup._id), params), _code=301)
class WikiApiController(WikiController): @validate(VModhash(), pageandprevious=VWikiPageRevise(('page', 'previous'), restricted=True), content=VMarkdown(('content'), renderer='wiki'), page_name=VWikiPageName('page'), reason=VPrintable('reason', 256)) @api_doc(api_section.wiki, uri='/api/wiki/edit') def POST_wiki_edit(self, pageandprevious, content, page_name, reason): page, previous = pageandprevious if not page: error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: self.handle_error(403, **(error.msg_params or {})) if not c.user._spam: page = WikiPage.create(c.site, page_name) if c.user._spam: error = _("You are doing that too much, please try again later.") self.handle_error(415, 'SPECIAL_ERRORS', special_errors=[error]) # Use the raw POST value as we need to tell the difference between # None/Undefined and an empty string. The validators use a default # value with both of those cases and would need to be changed. # In order to avoid breaking functionality, this was done instead. previous = previous._id if previous else request.post.get('previous') try: if page.name == 'config/stylesheet': report, parsed = c.site.parse_css(content, verify=False) if report is None: # g.css_killswitch self.handle_error(403, 'STYLESHEET_EDIT_DENIED') if report.errors: error_items = [x.message for x in sorted(report.errors)] self.handle_error(415, 'SPECIAL_ERRORS', special_errors=error_items) c.site.change_css(content, parsed, previous, reason=reason) else: try: page.revise(content, previous, c.user._id36, reason=reason) except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) setattr(c.site, "prev_" + ATTRIBUTE_BY_PAGE[page.name] + "_id", str(page.revision)) c.site._commit() if page.special or c.is_wiki_mod: description = modactions.get(page.name, 'Page %s edited' % page.name) ModAction.create(c.site, c.user, 'wikirevise', details=description) except ConflictException as e: self.handle_error(409, 'EDIT_CONFLICT', newcontent=e.new, newrevision=page.revision, diffcontent=e.htmldiff) return json.dumps({}) @validate(VModhash(), VWikiModerator(), page=VWikiPage('page'), act=VOneOf('act', ('del', 'add')), user=VExistingUname('username')) @api_doc(api_section.wiki, uri='/api/wiki/alloweditor/:act') def POST_wiki_allow_editor(self, act, page, user): if not user: self.handle_error(404, 'UNKNOWN_USER') elif act == 'del': page.remove_editor(user._id36) elif act == 'add': page.add_editor(user._id36) else: self.handle_error(400, 'INVALID_ACTION') return json.dumps({}) @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/hide') def POST_wiki_revision_hide(self, pv): page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') return json.dumps({'status': revision.toggle_hide()}) @validate(VModhash(), VWikiModerator(), pv=VWikiPageAndVersion(('page', 'revision'))) @api_doc(api_section.wiki, uri='/api/wiki/revert') def POST_wiki_revision_revert(self, pv): page, revision = pv if not revision: self.handle_error(400, 'INVALID_REVISION') content = revision.content reason = 'reverted back %s' % timesince(revision.date) if page.name == 'config/stylesheet': report, parsed = c.site.parse_css(content) if report.errors: self.handle_error(403, 'INVALID_CSS') c.site.change_css(content, parsed, prev=None, reason=reason, force=True) else: try: page.revise(content, author=c.user._id36, reason=reason, force=True) # continue storing the special pages as data attributes on the subreddit # object. TODO: change this to minimize subreddit get sizes. if page.special: setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content) setattr(c.site, "prev_" + ATTRIBUTE_BY_PAGE[page.name] + "_id", page.revision) c.site._commit() except ContentLengthError as e: self.handle_error(403, 'CONTENT_LENGTH_ERROR', max_length=e.max_length) return json.dumps({}) def pre(self): WikiController.pre(self) c.render_style = 'api' set_extension(request.environ, 'json')
class PromoteController(ListingController): where = 'promoted' render_cls = PromotePage @property def title_text(self): return _('promoted by you') def keep_fn(self): def keep(item): if item.promoted and not item._deleted: return True else: return False return keep def query(self): if c.user_is_sponsor: if self.sort == "future_promos": return queries.get_all_unapproved_links() elif self.sort == "pending_promos": return queries.get_all_accepted_links() elif self.sort == "unpaid_promos": return queries.get_all_unpaid_links() elif self.sort == "rejected_promos": return queries.get_all_rejected_links() elif self.sort == "live_promos": return queries.get_all_live_links() return queries.get_all_promoted_links() else: if self.sort == "future_promos": return queries.get_unapproved_links(c.user._id) elif self.sort == "pending_promos": return queries.get_accepted_links(c.user._id) elif self.sort == "unpaid_promos": return queries.get_unpaid_links(c.user._id) elif self.sort == "rejected_promos": return queries.get_rejected_links(c.user._id) elif self.sort == "live_promos": return queries.get_live_links(c.user._id) return queries.get_promoted_links(c.user._id) @validate(VSponsor()) def GET_listing(self, sort="", **env): if not c.user_is_loggedin or not c.user.email_verified: return self.redirect("/ad_inq") self.sort = sort return ListingController.GET_listing(self, **env) GET_index = GET_listing @validate(VSponsor()) def GET_new_promo(self): return PromotePage('content', content=PromoteLinkForm()).render() @validate(VSponsor('link'), link=VLink('link')) def GET_edit_promo(self, link): if not link or link.promoted is None: return self.abort404() rendered = wrap_links(link, wrapper=promote.sponsor_wrapper, skip=False) form = PromoteLinkForm(link=link, listing=rendered, timedeltatext="") page = PromotePage('new_promo', content=form) return page.render() # For development. Should eventually replace GET_edit_promo @validate(VSponsor('link'), link=VLink('link')) def GET_edit_promo_cpm(self, link): if not link or link.promoted is None: return self.abort404() rendered = wrap_links(link, wrapper=promote.sponsor_wrapper, skip=False) form = PromoteLinkFormCpm(link=link, listing=rendered, timedeltatext="") page = PromotePage('new_promo', content=form) return page.render() # admin only because the route might change @validate(VSponsorAdmin('campaign'), campaign=VPromoCampaign('campaign')) def GET_edit_promo_campaign(self, campaign): if not campaign: return self.abort404() link = Link._byID(campaign.link_id) return self.redirect(promote.promo_edit_url(link)) @validate(VSponsor(), dates=VDateRange(["startdate", "enddate"], max_range=timedelta(days=28), required=False)) def GET_graph(self, dates): start, end, bad_dates = _check_dates(dates) return PromotePage("graph", content=Promote_Graph( start, end, bad_dates=bad_dates) ).render() @validate(VSponsorAdmin(), dates=VDateRange(["startdate", "enddate"], max_range=timedelta(days=28), required=False)) def GET_admingraph(self, dates): start, end, bad_dates = _check_dates(dates) content = Promote_Graph(start, end, bad_dates=bad_dates, admin_view=True) if c.render_style == 'csv': return content.as_csv() return PromotePage("admingraph", content=content).render() def GET_inventory(self, sr_name): ''' Return available inventory data as json for use in ajax calls ''' inv_start_date = promote.promo_datetime_now() inv_end_date = inv_start_date + timedelta(60) inventory = promote.get_available_impressions( sr_name, inv_start_date, inv_end_date, fuzzed=(not c.user_is_admin) ) dates = [] impressions = [] max_imps = 0 for date, imps in inventory.iteritems(): dates.append(date.strftime("%m/%d/%Y")) impressions.append(imps) max_imps = max(max_imps, imps) return json.dumps({'sr':sr_name, 'dates': dates, 'imps':impressions, 'max_imps':max_imps}) # ## POST controllers below @validatedForm(VSponsorAdmin(), link=VLink("link_id"), campaign=VPromoCampaign("campaign_id36")) def POST_freebie(self, form, jquery, link, campaign): if promote.is_promo(link) and campaign: promote.free_campaign(link, campaign, c.user) form.redirect(promote.promo_edit_url(link)) @validatedForm(VSponsorAdmin(), link=VByName("link"), note=nop("note")) def POST_promote_note(self, form, jquery, link, note): if promote.is_promo(link): text = PromotionLog.add(link, note) form.find(".notes").children(":last").after( "<p>" + text + "</p>") @noresponse(VSponsorAdmin(), thing=VByName('id')) def POST_promote(self, thing): if promote.is_promo(thing): promote.accept_promotion(thing) @noresponse(VSponsorAdmin(), thing=VByName('id'), reason=nop("reason")) def POST_unpromote(self, thing, reason): if promote.is_promo(thing): promote.reject_promotion(thing, reason=reason) @validatedForm(VSponsor('link_id'), VModhash(), VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'), l=VLink('link_id'), title=VTitle('title'), url=VUrl('url', allow_self=False, lookup=False), ip=ValidIP(), disable_comments=VBoolean("disable_comments"), set_clicks=VBoolean("set_maximum_clicks"), max_clicks=VInt("maximum_clicks", min=0), set_views=VBoolean("set_maximum_views"), max_views=VInt("maximum_views", min=0), media_width=VInt("media-width", min=0), media_height=VInt("media-height", min=0), media_embed=VLength("media-embed", 1000), media_override=VBoolean("media-override"), domain_override=VLength("domain", 100) ) def POST_edit_promo(self, form, jquery, ip, l, title, url, disable_comments, set_clicks, max_clicks, set_views, max_views, media_height, media_width, media_embed, media_override, domain_override): should_ratelimit = False if not c.user_is_sponsor: set_clicks = False set_views = False should_ratelimit = True if not set_clicks: max_clicks = None if not set_views: max_views = None if not should_ratelimit: c.errors.remove((errors.RATELIMIT, 'ratelimit')) # demangle URL in canonical way if url: if isinstance(url, (unicode, str)): form.set_inputs(url=url) elif isinstance(url, tuple) or isinstance(url[0], Link): # there's already one or more links with this URL, but # we're allowing mutliple submissions, so we really just # want the URL url = url[0].url # users can change the disable_comments on promoted links if ((not l or not promote.is_promoted(l)) and (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG) or form.has_errors('url', errors.NO_URL, errors.BAD_URL) or jquery.has_errors('ratelimit', errors.RATELIMIT))): return if not l: l = promote.new_promotion(title, url, c.user, ip) elif promote.is_promo(l): changed = False # live items can only be changed by a sponsor, and also # pay the cost of de-approving the link trusted = c.user_is_sponsor or c.user.trusted_sponsor if not promote.is_promoted(l) or trusted: if title and title != l.title: l.title = title changed = not trusted if url and url != l.url: l.url = url changed = not trusted # only trips if the title and url are changed by a non-sponsor if changed and not promote.is_unpaid(l): promote.unapprove_promotion(l) if trusted and promote.is_unapproved(l): promote.accept_promotion(l) if c.user_is_sponsor: l.maximum_clicks = max_clicks l.maximum_views = max_views # comment disabling is free to be changed any time. l.disable_comments = disable_comments if c.user_is_sponsor or c.user.trusted_sponsor: if media_embed and media_width and media_height: l.media_object = dict(height=media_height, width=media_width, content=media_embed, type='custom') else: l.media_object = None l.media_override = media_override if getattr(l, "domain_override", False) or domain_override: l.domain_override = domain_override l._commit() form.redirect(promote.promo_edit_url(l)) @validate(VSponsorAdmin()) def GET_roadblock(self): return PromotePage('content', content=Roadblocks()).render() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), sr=VSubmitSR('sr', promotion=True)) def POST_add_roadblock(self, form, jquery, dates, sr): if (form.has_errors('startdate', errors.BAD_DATE, errors.BAD_FUTURE_DATE) or form.has_errors('enddate', errors.BAD_DATE, errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)): return if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): return if dates and sr: sd, ed = dates promote.roadblock_reddit(sr.name, sd.date(), ed.date()) jquery.refresh() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), sr=VSubmitSR('sr', promotion=True)) def POST_rm_roadblock(self, form, jquery, dates, sr): if dates and sr: sd, ed = dates promote.unroadblock_reddit(sr.name, sd.date(), ed.date()) jquery.refresh() @validatedForm(VSponsor('link_id'), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), l=VLink('link_id'), bid=VFloat('bid', min=0, max=g.max_promote_bid, coerce=False, error=errors.BAD_BID), sr=VSubmitSR('sr', promotion=True), campaign_id36=nop("campaign_id36"), targeting=VLength("targeting", 10)) def POST_edit_campaign(self, form, jquery, l, campaign_id36, dates, bid, sr, targeting): if not l: return start, end = dates or (None, None) if (start and end and not promote.is_accepted(l) and not c.user_is_sponsor): # if the ad is not approved already, ensure the start date # is at least 2 days in the future start = start.date() end = end.date() now = promote.promo_datetime_now() future = make_offset_date(now, g.min_promote_future, business_days=True) if start < future.date(): c.errors.add(errors.BAD_FUTURE_DATE, msg_params=dict(day=g.min_promote_future), field="startdate") if (form.has_errors('startdate', errors.BAD_DATE, errors.BAD_FUTURE_DATE) or form.has_errors('enddate', errors.BAD_DATE, errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)): return # Limit the number of PromoCampaigns a Link can have # Note that the front end should prevent the user from getting # this far existing_campaigns = list(PromoCampaign._by_link(l._id)) if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK: c.errors.add(errors.TOO_MANY_CAMPAIGNS, msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK}, field='title') form.has_errors('title', errors.TOO_MANY_CAMPAIGNS) return duration = max((end - start).days, 1) if form.has_errors('bid', errors.BAD_BID): return # minimum bid depends on user privilege and targeting, checked here # instead of in the validator b/c current duration is needed if c.user_is_sponsor: min_daily_bid = 0 elif targeting == 'one': min_daily_bid = g.min_promote_bid * 1.5 else: min_daily_bid = g.min_promote_bid if campaign_id36: # you cannot edit the bid of a live ad unless it's a freebie try: campaign = PromoCampaign._byID36(campaign_id36) if (bid != campaign.bid and campaign.start_date < datetime.now(g.tz) and not campaign.is_freebie()): c.errors.add(errors.BID_LIVE, field='bid') form.has_errors('bid', errors.BID_LIVE) return except NotFound: pass if bid is None or bid / duration < min_daily_bid: c.errors.add(errors.BAD_BID, field='bid', msg_params={'min': min_daily_bid, 'max': g.max_promote_bid}) form.has_errors('bid', errors.BAD_BID) return if targeting == 'one': if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): # checking to get the error set in the form, but we can't # check for rate-limiting if there's no subreddit return oversold = promote.is_roadblocked(sr.name, start, end) if oversold and not c.user_is_sponsor: msg_params = {"start": oversold[0].strftime('%m/%d/%Y'), "end": oversold[1].strftime('%m/%d/%Y')} c.errors.add(errors.OVERSOLD, field='sr', msg_params=msg_params) form.has_errors('sr', errors.OVERSOLD) return if targeting == 'none': sr = None if campaign_id36 is not None: campaign = PromoCampaign._byID36(campaign_id36) promote.edit_campaign(l, campaign, dates, bid, sr) r = promote.get_renderable_campaigns(l, campaign) jquery.update_campaign(r.campaign_id36, r.start_date, r.end_date, r.duration, r.bid, r.sr, r.status) else: campaign = promote.new_campaign(l, dates, bid, sr) r = promote.get_renderable_campaigns(l, campaign) jquery.new_campaign(r.campaign_id36, r.start_date, r.end_date, r.duration, r.bid, r.sr, r.status) @validatedForm(VSponsor('link_id'), VModhash(), l=VLink('link_id'), campaign=VPromoCampaign("campaign_id36")) def POST_delete_campaign(self, form, jquery, l, campaign): if l and campaign: promote.delete_campaign(l, campaign) @validatedForm(VSponsor('container'), VModhash(), user=VExistingUname('name'), thing=VByName('container')) def POST_traffic_viewer(self, form, jquery, user, thing): """ Adds a user to the list of users allowed to view a promoted link's traffic page. """ if not form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER): form.set_inputs(name="") form.set_html(".status:first", _("added")) if promote.add_traffic_viewer(thing, user): user_row = TrafficViewerList(thing).user_row('traffic', user) jquery("#traffic-table").show( ).find("table").insert_table_rows(user_row) # send the user a message msg = user_added_messages['traffic']['pm']['msg'] subj = user_added_messages['traffic']['pm']['subject'] if msg and subj: d = dict(url=thing.make_permalink_slow(), traffic_url=promote.promo_traffic_url(thing), title=thing.title) msg = msg % d item, inbox_rel = Message._new(c.user, user, subj, msg, request.ip) queries.new_message(item, inbox_rel) @validatedForm(VSponsor('container'), VModhash(), iuser=VByName('id'), thing=VByName('container')) def POST_rm_traffic_viewer(self, form, jquery, iuser, thing): if thing and iuser: promote.rm_traffic_viewer(thing, iuser) @validatedForm(VSponsor('link'), link=VByName("link"), campaign=VPromoCampaign("campaign"), customer_id=VInt("customer_id", min=0), pay_id=VInt("account", min=0), edit=VBoolean("edit"), address=ValidAddress( ["firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber"], allowed_countries=g.allowed_pay_countries), creditcard=ValidCard(["cardNumber", "expirationDate", "cardCode"])) def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id, edit, address, creditcard): address_modified = not pay_id or edit form_has_errors = False if address_modified: if (form.has_errors(["firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber"], errors.BAD_ADDRESS) or form.has_errors(["cardNumber", "expirationDate", "cardCode"], errors.BAD_CARD)): form_has_errors = True elif g.authorizenetapi: pay_id = edit_profile(c.user, address, creditcard, pay_id) else: pay_id = 1 # if link is in use or finished, don't make a change if pay_id and not form_has_errors: # valid bid and created or existing bid id. # check if already a transaction if g.authorizenetapi: success, reason = promote.auth_campaign(link, campaign, c.user, pay_id) else: success = True if success: form.redirect(promote.promo_edit_url(link)) else: form.set_html(".status", reason or _("failed to authenticate card. sorry.")) @validate(VSponsor("link"), link=VLink("link"), campaign=VPromoCampaign("campaign")) def GET_pay(self, link, campaign): # no need for admins to play in the credit card area if c.user_is_loggedin and c.user._id != link.author_id: return self.abort404() if not campaign.link_id == link._id: return self.abort404() if g.authorizenetapi: data = get_account_info(c.user) content = PaymentForm(link, campaign, customer_id=data.customerProfileId, profiles=data.paymentProfiles, max_profiles=PROFILE_LIMIT) else: content = None res = LinkInfoPage(link=link, content=content, show_sidebar=False) return res.render() def GET_link_thumb(self, *a, **kw): """ See GET_upload_sr_image for rationale """ return "nothing to see here." @validate(VSponsor("link_id"), link=VByName('link_id'), file=VLength('file', 500 * 1024)) def POST_link_thumb(self, link=None, file=None): if link and (not promote.is_promoted(link) or c.user_is_sponsor or c.user.trusted_sponsor): errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="") try: # thumnails for promoted links can change and therefore expire force_thumbnail(link, file, file_type=".jpg") except cssfilter.BadImage: # if the image doesn't clean up nicely, abort errors["IMAGE_ERROR"] = _("bad image") if any(errors.values()): return UploadedImage("", "", "upload", errors=errors, form_id="image-upload").render() else: link._commit() return UploadedImage(_('saved'), thumbnail_url(link), "", errors=errors, form_id="image-upload").render() @validate(VSponsorAdmin(), launchdate=VDate('ondate'), dates=VDateRange(['startdate', 'enddate']), query_type=VOneOf('q', ('started_on', 'between'), default=None)) def GET_admin(self, launchdate=None, dates=None, query_type=None): return PromoAdminTool(query_type=query_type, launchdate=launchdate, start=dates[0], end=dates[1]).render()